티스토리 뷰
Ory Oathkeeper를 활용한 Zero Trust IAP 구현 가이드
작성일: 2025년 12월 6일
카테고리: Authentication, Authorization, API Gateway, Security
키워드: Ory Oathkeeper, Zero Trust, IAP, Identity Aware Proxy, Envoy, ext_authz
요약
Ory Oathkeeper는 Zero Trust Identity & Access Proxy로, 모든 요청에 대해 인증/인가를 수행하는 게이트키퍼 역할을 한다. Envoy Gateway와 연동하여 ext_authz 패턴으로 동작하며, Kratos 세션과 Hydra OAuth2 토큰을 모두 검증할 수 있다. 이 글에서는 ImpRun 프로젝트에서 Oathkeeper를 활용한 Zero Trust 아키텍처 구현 사례를 다룬다.
Ory Oathkeeper 개요
Oathkeeper란
Oathkeeper는 Ory 프로젝트의 Identity & Access Proxy (IAP) 컴포넌트다. 모든 요청을 가로채서 인증/인가를 수행한 후, 통과된 요청만 업스트림 서비스로 전달한다.
flowchart LR
Client["Client"]
Oathkeeper["Oathkeeper<br/>(IAP)"]
Kratos["Kratos<br/>(Session)"]
Hydra["Hydra<br/>(OAuth2)"]
Upstream["Upstream<br/>Service"]
Client -->|"Request"| Oathkeeper
Oathkeeper -->|"Verify Session"| Kratos
Oathkeeper -->|"Introspect Token"| Hydra
Oathkeeper -->|"Forward + Headers"| Upstream
style Oathkeeper stroke:#dc2626,stroke-width:3px
style Kratos stroke:#16a34a,stroke-width:2px
style Hydra stroke:#2563eb,stroke-width:2px
Zero Trust 원칙
Oathkeeper는 Zero Trust 보안 모델을 구현한다:
| 원칙 | 설명 |
|---|---|
| Never Trust, Always Verify | 모든 요청을 검증, 내부 네트워크도 신뢰하지 않음 |
| Least Privilege | 필요한 최소 권한만 부여 |
| Assume Breach | 침해를 가정하고 설계 |
핵심 컴포넌트
Oathkeeper는 4가지 핵심 컴포넌트로 구성된다:
Request → Authenticator → Authorizer → Mutator → Upstream| 컴포넌트 | 역할 |
|---|---|
| Authenticator | 요청자 신원 확인 (Session, Bearer Token, JWT 등) |
| Authorizer | 접근 권한 확인 (Allow, Deny, Remote) |
| Mutator | 요청 변환 (Header 추가, JWT 생성 등) |
| Error Handler | 에러 응답 처리 (JSON, Redirect 등) |
Oathkeeper 설정
기본 설정 (oathkeeper.yml)
serve:
proxy:
port: 4455
api:
port: 4456
prometheus:
port: 9000
access_rules:
matching_strategy: regexp
repositories:
- file:///etc/config/oathkeeper/access-rules.yml
Authenticators 설정
Oathkeeper는 여러 인증 방식을 지원한다:
authenticators:
# 인증 없음 (Public endpoints)
noop:
enabled: true
# Kratos 세션 쿠키 검증
cookie_session:
enabled: true
config:
check_session_url: http://kratos:4433/sessions/whoami
preserve_path: true
extra_from: "@this"
subject_from: "identity.id"
only:
- ory_kratos_session
# OAuth2 Bearer Token 검증
bearer_token:
enabled: true
config:
check_session_url: http://hydra:4445/oauth2/introspect
token_from:
header: Authorization
preserve_host: true
# JWT 토큰 검증
jwt:
enabled: true
config:
jwks_urls:
- http://hydra:4444/.well-known/jwks.json
scope_strategy: hierarchic
Authorizers 설정
authorizers:
# 모든 요청 허용
allow:
enabled: true
# 모든 요청 거부
deny:
enabled: true
# 원격 서버에 권한 확인 (Keto 연동 가능)
remote:
enabled: true
config:
remote: http://keto:4466/relation-tuples/check
headers:
Content-Type: application/json
Mutators 설정
인증된 요청에 사용자 정보를 추가한다:
mutators:
# 변환 없음
noop:
enabled: true
# HTTP 헤더 추가
header:
enabled: true
config:
headers:
X-User-Id: "{{ print .Subject }}"
X-User-Email: "{{ print .Extra.identity.traits.email }}"
X-User-Roles: "{{ print .Extra.identity.metadata_public.roles }}"
# ID Token (JWT) 생성
id_token:
enabled: true
config:
issuer_url: http://oathkeeper:4455
jwks_url: file:///etc/config/oathkeeper/jwks.json
claims: |
{
"sub": "{{ print .Subject }}",
"email": "{{ print .Extra.identity.traits.email }}",
"roles": {{ .Extra.identity.metadata_public.roles | toJson }}
}
Error Handlers 설정
errors:
fallback:
- json
handlers:
# JSON 에러 응답
json:
enabled: true
config:
verbose: true
# 로그인 페이지로 리다이렉트
redirect:
enabled: true
config:
to: http://localhost:3000/auth/login
when:
- error:
- unauthorized
- forbidden
request:
header:
accept:
- text/html
Access Rules
Rule 구조
Access Rule은 URL 패턴별로 인증/인가 정책을 정의한다:
- id: "rule-name"
upstream:
url: "http://backend:3000"
preserve_host: true
match:
url: "<(http|https)>://<.*>/api/<path>"
methods:
- GET
- POST
authenticators:
- handler: cookie_session
- handler: bearer_token
authorizer:
handler: allow
mutators:
- handler: header
Public Endpoints
인증이 필요 없는 엔드포인트:
- id: "public-api-directory"
upstream:
url: "http://api:3000"
match:
url: "<(http|https)>://<.*>/api/directory/<.*>"
methods:
- GET
authenticators:
- handler: noop
authorizer:
handler: allow
mutators:
- handler: noop
Protected Endpoints
세션 또는 Bearer Token이 필요한 엔드포인트:
- id: "protected-api"
upstream:
url: "http://api:3000"
match:
url: "<(http|https)>://<.*>/api/<(?!directory).*>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
# 순서대로 시도, 첫 번째 성공한 것 사용
- handler: cookie_session
- handler: bearer_token
authorizer:
handler: allow
mutators:
- handler: header
Gateway Proxy
외부 API Gateway로 프록시하는 경우:
- id: "gateway-proxy"
upstream:
url: "http://upstream-service:8080"
preserve_host: true
match:
url: "<(http|https)>://<.*>/gateway/<.*>"
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
authenticators:
- handler: bearer_token
authorizer:
handler: allow
mutators:
# JWT 토큰 생성하여 업스트림에 전달
- handler: id_token
인증 플로우 타입
ImpRun에서 지원하는 인증 플로우 타입:
| flow_type | 설명 | Oathkeeper Handler |
|---|---|---|
public |
인증 불필요 | noop |
authorization-code |
OAuth2 웹 로그인 | cookie_session |
client-credentials |
M2M 토큰 | bearer_token (introspect) |
jwt-auth |
JWT 토큰 검증 | jwt |
api-key |
API Key 인증 | bearer_token |
flowchart TB
subgraph flows["Authentication Flows"]
public["public<br/>인증 없음"]
authCode["authorization-code<br/>OAuth2 웹 로그인"]
clientCred["client-credentials<br/>M2M 토큰"]
jwt["jwt-auth<br/>JWT 검증"]
apiKey["api-key<br/>API Key"]
end
subgraph handlers["Oathkeeper Handlers"]
noop["noop"]
cookieSession["cookie_session"]
bearerToken["bearer_token"]
jwtHandler["jwt"]
end
public --> noop
authCode --> cookieSession
clientCred --> bearerToken
jwt --> jwtHandler
apiKey --> bearerToken
style flows stroke:#2563eb,stroke-width:2px
style handlers stroke:#16a34a,stroke-width:2px
Envoy Gateway 연동
ext_authz 패턴
Oathkeeper는 Envoy의 External Authorization (ext_authz) 필터와 연동된다:
sequenceDiagram
participant Client
participant Envoy as Envoy Gateway
participant Oathkeeper
participant Kratos
participant Hydra
participant Upstream as Upstream API
Client->>Envoy: 1. API Request
Envoy->>Oathkeeper: 2. ext_authz check
alt Session Cookie
Oathkeeper->>Kratos: 3a. /sessions/whoami
Kratos-->>Oathkeeper: session info
else Bearer Token
Oathkeeper->>Hydra: 3b. /oauth2/introspect
Hydra-->>Oathkeeper: token info
end
alt Authorized
Oathkeeper-->>Envoy: 4. OK + headers
Envoy->>Upstream: 5. Forward request + X-User headers
Upstream-->>Envoy: response
Envoy-->>Client: response
else Unauthorized
Oathkeeper-->>Envoy: 4. 401/403
Envoy-->>Client: error response
end
Envoy 설정
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: upstream_service
http_filters:
# External Authorization with Oathkeeper
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: oathkeeper
timeout: 0.5s
transport_api_version: V3
failure_mode_allow: false
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: oathkeeper
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: oathkeeper
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: oathkeeper
port_value: 4456
- name: upstream_service
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: upstream_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: api
port_value: 3000
아키텍처 다이어그램
flowchart TB
subgraph client["Clients"]
Browser["Browser<br/>(Session Cookie)"]
APIClient["API Client<br/>(Bearer Token)"]
end
subgraph gateway["Gateway Layer"]
Envoy["Envoy Gateway<br/>:8080"]
Oathkeeper["Oathkeeper<br/>:4455/:4456"]
end
subgraph auth["Auth Layer"]
Kratos["Kratos<br/>:4433/:4434"]
Hydra["Hydra<br/>:4444/:4445"]
end
subgraph backend["Backend"]
API["Go API<br/>:8080"]
Upstream["Upstream Services"]
end
Browser -->|"Cookie"| Envoy
APIClient -->|"Bearer"| Envoy
Envoy <-->|"ext_authz gRPC"| Oathkeeper
Oathkeeper -->|"whoami"| Kratos
Oathkeeper -->|"introspect"| Hydra
Envoy -->|"X-User-* headers"| API
Envoy -->|"JWT / headers"| Upstream
style gateway stroke:#dc2626,stroke-width:2px
style auth stroke:#16a34a,stroke-width:2px
백엔드에서 인증 정보 사용
X-User 헤더 활용
Oathkeeper가 추가한 헤더를 백엔드에서 활용한다:
// internal/interface/middleware/auth_from_headers.go
func AuthFromHeadersMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Oathkeeper가 추가한 헤더에서 사용자 정보 추출
userID := c.GetHeader("X-User-Id")
userEmail := c.GetHeader("X-User-Email")
userRoles := c.GetHeader("X-User-Roles")
if userID == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "No user information in request",
})
return
}
// Context에 사용자 정보 저장
c.Set("user", &UserInfo{
ID: userID,
Email: userEmail,
Roles: parseRoles(userRoles),
})
c.Next()
}
}
func parseRoles(rolesHeader string) []string {
if rolesHeader == "" {
return []string{}
}
// JSON 배열 또는 쉼표 구분 문자열 파싱
var roles []string
if err := json.Unmarshal([]byte(rolesHeader), &roles); err != nil {
return strings.Split(rolesHeader, ",")
}
return roles
}
Dual Mode 지원
Oathkeeper 사용 여부에 따라 동적으로 인증 처리:
func AuthMiddleware(kratosClient *client.KratosClient, useOathkeeper bool) gin.HandlerFunc {
return func(c *gin.Context) {
// Oathkeeper 모드: 헤더에서 사용자 정보 추출
if useOathkeeper {
userID := c.GetHeader("X-User-Id")
if userID != "" {
c.Set("user", &UserInfo{ID: userID})
c.Next()
return
}
}
// Direct 모드: Kratos 세션 직접 검증
sessionCookie, err := c.Cookie("ory_kratos_session")
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "No authentication provided",
})
return
}
session, err := kratosClient.WhoAmI(c.Request.Context(), sessionCookie)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid session",
})
return
}
c.Set("user", &UserInfo{
ID: session.Identity.ID,
Email: session.Identity.Traits.Email,
})
c.Next()
}
}
JWKS 설정
키 생성
ID Token mutator를 사용하려면 JWKS를 생성해야 한다:
# RSA 키 쌍 생성
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem
# JWKS 형식으로 변환 (jose 라이브러리 사용)
# 또는 Oathkeeper의 자동 생성 기능 사용
jwks.json 예시
{
"keys": [
{
"kty": "RSA",
"kid": "oathkeeper-key-1",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB",
"d": "...",
"p": "...",
"q": "...",
"dp": "...",
"dq": "...",
"qi": "..."
}
]
}
Docker Compose 설정
services:
oathkeeper:
image: oryd/oathkeeper:v0.40.7
command: serve --config /etc/config/oathkeeper/oathkeeper.yml
ports:
- "4455:4455" # Proxy
- "4456:4456" # API
volumes:
- ./docker/ory/oathkeeper:/etc/config/oathkeeper
environment:
- LOG_LEVEL=debug
depends_on:
- kratos
- hydra
envoy:
image: envoyproxy/envoy:v1.32-latest
ports:
- "8080:8080" # Gateway
- "9901:9901" # Admin
volumes:
- ./docker/envoy/envoy.yaml:/etc/envoy/envoy.yaml
depends_on:
- oathkeeper
운영 고려사항
성능
- Oathkeeper는 stateless하므로 수평 확장 가능
- 세션 검증 결과 캐싱으로 Kratos/Hydra 부하 감소
- Envoy와 gRPC 통신으로 낮은 지연시간
보안
- Admin API 접근 제한: 4456 포트는 내부 네트워크에서만 접근
- JWKS 키 관리: 프로덕션에서는 외부 키 관리 시스템 사용 권장
- failure_mode_allow: false로 설정하여 Oathkeeper 장애 시 요청 차단
모니터링
Prometheus 메트릭 제공:
serve:
prometheus:
port: 9000
path: /metrics
주요 메트릭:
ory_oathkeeper_requests_totalory_oathkeeper_request_duration_secondsory_oathkeeper_authenticator_*
참고 자료
공식 문서
관련 문서
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| Claude Code 사용 리포트: 27일간 12억 토큰의 기록 (0) | 2025.12.11 |
|---|---|
| ImpRun 인증/인가 아키텍처: Ory 스택 통합 구현 가이드 (0) | 2025.12.07 |
| Ory Keto를 활용한 ReBAC 기반 권한 관리 시스템 구축 (0) | 2025.12.07 |
| Ory Kratos를 활용한 사용자 인증 시스템 구축: ImpRun 프로젝트 적용 사례 (0) | 2025.12.07 |
| Ory Hydra를 활용한 OAuth2/OIDC 서버 구축 가이드: 실제 프로젝트 적용 사례 (0) | 2025.12.07 |
- Total
- Today
- Yesterday
- 개발 도구
- Next.js
- LangChain
- Developer Tools
- security
- backend
- AI agent
- Claude
- authentication
- SHACL
- PYTHON
- Kubernetes
- LLM
- Rag
- architecture
- ai 개발 도구
- Go
- Tax Analysis
- claude code
- authorization
- react
- AI
- Tailwind CSS
- AI Development
- api gateway
- knowledge graph
- workflow
- troubleshooting
- Ontology
- frontend
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |