Skip to content

필사 모드: API Gateway에서의 OIDC 토큰 검증 — Istio, Envoy, Gateway API 실전

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — 토큰 검증은 어디서 해야 하는가

마이크로서비스 아키텍처에서 가장 자주 반복되는 보안 논쟁 중 하나는 "JWT 검증을 어디서 할 것인가"입니다. 게이트웨이에서 한 번만? 모든 서비스에서? 둘 다? 2026년 현재 이 질문에 대한 업계의 합의는 비교적 명확해졌습니다. **엣지에서 거르고, 서비스에서 다시 검증한다(defense in depth)** 입니다. 그리고 그 구현 수단으로 Envoy 기반 스택(Istio, Envoy Gateway, Gloo 등)이 사실상의 표준이 되었습니다.

이 글에서는 Envoy의 jwt_authn 필터를 바닥부터 이해하고, Istio의 RequestAuthentication + AuthorizationPolicy 조합을 풍부한 YAML 예제로 살펴봅니다. JWKS 캐싱과 장애 모드, audience 전략, 토큰 전파 패턴([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) Token Exchange 포함) 같은 운영 난제를 짚고, Kong/APISIX의 OIDC 플러그인 비교, Gateway API 시대의 인증 표준화 흐름, mTLS와 JWT의 조합까지 다룹니다. 마지막에는 401 디버깅 플로우차트를 ASCII로 정리합니다.

엣지에서 검증 vs 서비스에서 검증

두 접근의 트레이드오프를 먼저 정리합니다.

| 항목 | 엣지(게이트웨이)에서만 검증 | 각 서비스에서만 검증 |

| ------------- | ---------------------------------- | --------------------------------- |

| 성능 | 검증 1회, 내부는 무비용 | 홉마다 검증 비용 반복 |

| 일관성 | 중앙 정책, 설정 한 곳 | 서비스별 라이브러리/설정 파편화 |

| 내부 침해 대응 | 게이트웨이 우회 시 무방비 | 내부 트래픽도 검증되어 견고 |

| 클레임 활용 | 헤더로 전달 필요(위조 위험 관리) | 서비스가 직접 클레임 접근 |

| 운영 부담 | 낮음 | 라이브러리 버전, JWKS 관리 분산 |

결론은 둘의 조합입니다. 실무 권장 패턴은 다음과 같습니다.

1. **엣지(게이트웨이)**: 서명, 발급자(iss), 만료(exp), audience(aud)를 검증하고 잘못된 트래픽을 일찍 차단합니다. 비싼 내부 자원이 쓰레기 토큰에 낭비되지 않습니다.

2. **서비스(사이드카 또는 라이브러리)**: 같은 검증을 반복하되, 서비스별 audience와 세밀한 인가(스코프, 역할)를 추가합니다. 게이트웨이가 뚫리거나 내부에서 위조된 호출이 와도 방어됩니다.

3. **서비스 간 신뢰는 mTLS로**: 사용자 토큰과 별개로, 호출자 서비스의 신원은 mTLS(SPIFFE 등)로 증명합니다. 뒤에서 다시 다룹니다.

[엣지: 1차 검증 - 서명/iss/exp/aud]

Client ──> API Gateway (Envoy jwt_authn) ──┐

│ mTLS (서비스 신원)

v

[서비스: 2차 검증 + 세밀한 인가]

Service A (sidecar RequestAuthentication)

│ 토큰 전파 or exchange

v

Service B (sidecar + AuthorizationPolicy)

Envoy jwt_authn 필터 상세

Istio든 Envoy Gateway든 Kong의 일부 모드든, 밑바닥에서 JWT를 검증하는 것은 Envoy의 HTTP 필터인 jwt_authn입니다. 원리를 알면 상위 추상화의 동작과 장애를 정확히 이해할 수 있습니다.

핵심 개념은 두 가지입니다.

- **providers** — "어떤 발급자의 토큰을, 어떤 키로, 어떻게 검증할 것인가"의 정의. issuer, audiences, JWKS 소스, 토큰 추출 위치, 페이로드 전달 방식을 지정합니다.

- **rules** — "어떤 라우트에 어떤 provider를 요구할 것인가"의 매핑. requires로 provider를 지정하며 allow_missing, allow_missing_or_failed 같은 완화 모드도 있습니다.

전체 설정 예시는 다음과 같습니다.

http_filters:

- name: envoy.filters.http.jwt_authn

typed_config:

'@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication

providers:

keycloak_provider:

issuer: https://keycloak.example.com/realms/prod

audiences:

- orders-api

remote_jwks:

http_uri:

uri: https://keycloak.example.com/realms/prod/protocol/openid-connect/certs

cluster: keycloak_jwks_cluster

timeout: 3s

cache_duration: 600s

async_fetch:

fast_listener: false

retry_policy:

num_retries: 3

Authorization: Bearer 헤더에서 추출 (기본값)

from_headers:

- name: Authorization

value_prefix: 'Bearer '

검증된 페이로드를 메타데이터로 저장해 후속 필터(RBAC 등)가 활용

payload_in_metadata: jwt_payload

검증 후 업스트림에 평문 클레임 헤더로 전달

claim_to_headers:

- header_name: x-jwt-sub

claim_name: sub

- header_name: x-jwt-scope

claim_name: scope

업스트림으로 원본 토큰을 제거할지 유지할지

forward: true

exp 검증의 클럭 스큐 허용

clock_skew_seconds: 30

rules:

헬스체크는 토큰 불요

- match:

prefix: /healthz

공개 문서 엔드포인트는 토큰이 있으면 검증, 없어도 통과

- match:

prefix: /docs

requires:

requires_any:

requirements:

- provider_name: keycloak_provider

- allow_missing: {}

나머지는 모두 필수

- match:

prefix: /

requires:

provider_name: keycloak_provider

설정 항목별 주의점은 다음과 같습니다.

- **issuer는 토큰의 iss 클레임과 문자열 단위로 정확히 일치**해야 합니다. 후행 슬래시 하나 차이로 401이 납니다.

- **remote_jwks의 cluster는 별도로 정의**되어야 합니다. Envoy는 JWKS 엔드포인트도 클러스터로 추상화하므로, DNS/TLS 설정이 빠지면 키를 못 가져와 모든 요청이 401이 됩니다.

- **async_fetch를 켜면** 리스너 기동 시 JWKS를 미리 받아오고 백그라운드에서 갱신합니다. 첫 요청 지연과 JWKS 엔드포인트 순단의 영향을 줄입니다.

- **forward: true**가 없으면 기본 동작으로 Authorization 헤더가 업스트림에 제거될 수 있습니다(설정 계열에 따라 다름). 토큰 전파가 필요하면 명시하세요.

- **claim_to_headers는 평문 헤더**입니다. 업스트림은 이 헤더를 신뢰하기 전에 "Envoy를 거치지 않은 트래픽이 불가능한가"를 네트워크 수준에서 보장해야 합니다.

Istio — RequestAuthentication + AuthorizationPolicy

Istio는 위의 jwt_authn을 RequestAuthentication CRD로, 인가를 AuthorizationPolicy CRD로 추상화합니다. 가장 중요한 사실 하나를 먼저 강조합니다.

**RequestAuthentication 단독으로는 아무것도 막지 않습니다.** 이 리소스는 "토큰이 있으면 검증하라"는 의미일 뿐, 토큰이 아예 없는 요청은 통과시킵니다. 차단하려면 반드시 AuthorizationPolicy로 "유효한 주체(requestPrincipals)가 있는 요청만 허용"을 선언해야 합니다. 이 함정으로 인한 보안 사고가 실제로 빈번합니다.

ingress gateway에서의 1차 검증 설정입니다.

apiVersion: security.istio.io/v1

kind: RequestAuthentication

metadata:

name: ingress-jwt

namespace: istio-system

spec:

selector:

matchLabels:

istio: ingressgateway

jwtRules:

- issuer: https://keycloak.example.com/realms/prod

jwksUri: https://keycloak.example.com/realms/prod/protocol/openid-connect/certs

audiences:

- api-gateway

forwardOriginalToken: true

outputClaimToHeaders:

- header: x-jwt-sub

claim: sub

apiVersion: security.istio.io/v1

kind: AuthorizationPolicy

metadata:

name: ingress-require-jwt

namespace: istio-system

spec:

selector:

matchLabels:

istio: ingressgateway

action: DENY

rules:

- from:

- source:

notRequestPrincipals: ['*']

to:

- operation:

notPaths: ['/healthz', '/metrics']

DENY + notRequestPrincipals 패턴은 "유효한 토큰 주체가 없는 요청을 거부"하는 표준 관용구입니다. requestPrincipals의 값은 iss와 sub를 슬래시로 이은 형태가 됩니다.

서비스 계층에서는 더 세밀한 인가를 겁니다. orders 서비스에 "쓰기 작업은 orders:write 스코프 필요"를 표현하면 다음과 같습니다.

apiVersion: security.istio.io/v1

kind: RequestAuthentication

metadata:

name: orders-jwt

namespace: orders

spec:

selector:

matchLabels:

app: orders

jwtRules:

- issuer: https://keycloak.example.com/realms/prod

jwksUri: https://keycloak.example.com/realms/prod/protocol/openid-connect/certs

audiences:

- orders-api

apiVersion: security.istio.io/v1

kind: AuthorizationPolicy

metadata:

name: orders-authz

namespace: orders

spec:

selector:

matchLabels:

app: orders

action: ALLOW

rules:

읽기: 인증된 주체면 허용

- from:

- source:

requestPrincipals: ['https://keycloak.example.com/realms/prod/*']

to:

- operation:

methods: ['GET']

paths: ['/orders', '/orders/*']

쓰기: orders:write 스코프 요구

- from:

- source:

requestPrincipals: ['https://keycloak.example.com/realms/prod/*']

to:

- operation:

methods: ['POST', 'PUT', 'DELETE']

paths: ['/orders', '/orders/*']

when:

- key: request.auth.claims[scope]

values: ['*orders:write*']

ALLOW 정책이 하나라도 존재하는 워크로드는 "매칭되지 않는 모든 요청 거부"로 동작이 바뀐다는 점도 기억해야 합니다. 정책을 추가하는 순간 기본값이 deny-by-default로 전환됩니다.

JWKS 캐싱과 장애 모드

JWT 검증 시스템의 가용성은 JWKS 엔드포인트 관리에서 갈립니다. 장애 시나리오를 표로 정리합니다.

| 시나리오 | 증상 | 대응 |

| -------------------------------- | ------------------------------------- | ----------------------------------------------------- |

| JWKS 엔드포인트 순단 | 캐시 만료 후 fetch 실패 → 전면 401 | async_fetch + 캐시 TTL 여유, IdP 다중화 |

| 키 롤테이션 직후 | 새 kid 토큰이 캐시 미스로 401 | IdP는 새 키 공개 후 유예기간을 두고 서명 전환 |

| IdP가 옛 키를 즉시 삭제 | 기존 토큰 전부 401 | 키 retire는 최소 토큰 수명 이상 지연 |

| 게이트웨이 재기동 + IdP 다운 | JWKS 초기 fetch 실패 | fail-open 여부 정책 결정, JWKS를 로컬 파일로 폴백 |

| 클럭 스큐 | 간헐적 exp/nbf 검증 실패 | clock_skew_seconds 설정 + NTP 모니터링 |

운영 권장 사항은 다음과 같습니다.

- 캐시 TTL은 IdP의 키 롤테이션 주기와 함께 설계합니다. 예: 롤테이션 24시간 전 새 키 공개, 캐시 10분 — 이러면 캐시가 낡아도 검증이 깨지지 않습니다.

- Envoy의 local_jwks(파일 기반)를 비상용으로 준비해 두면 IdP 완전 장애 시에도 기존 키로 검증을 지속할 수 있습니다. 단, 폐기된 키가 살아남는 위험과 트레이드오프입니다.

- JWKS fetch 실패율, 캐시 히트율, 401 비율을 메트릭으로 노출하고 알람을 겁니다. Envoy는 jwt_authn 통계(denied, jwks_fetch_failed 등)를 제공합니다.

Audience 전략

aud 클레임은 "이 토큰이 누구를 위한 것인가"를 선언합니다. 전략 선택지는 세 가지입니다.

1. **단일 audience (게이트웨이용 하나)** — 구현이 단순하지만, 토큰이 어느 서비스에서나 재사용 가능해 토큰 탈취 시 폭발 반경이 큽니다.

2. **서비스별 audience** — 각 서비스가 자기 audience만 수락합니다. 가장 안전하지만, 클라이언트가 서비스마다 다른 토큰을 받아야 하고 서비스 간 호출 시 token exchange가 필요해집니다.

3. **계층형(현실적 절충)** — 외부 노출 API 단위로 audience를 나누고, 내부 세분화는 스코프로 처리합니다. 게이트웨이는 광역 audience를, 각 서비스는 자신이 속한 API의 audience + 스코프를 검증합니다.

권장 원칙은 다음과 같습니다.

- 최소한 "이 조직의 토큰이면 다 통과" 같은 audience 미검증 상태는 피합니다. RFC 9700(OAuth Security BCP)도 audience restriction을 핵심 완화책으로 명시합니다.

- access token의 audience는 리소스 서버 기준으로, ID token(aud=클라이언트)은 API 호출에 쓰지 않습니다. ID token을 API에 보내는 것은 흔한 안티패턴입니다.

토큰 전파 — 원 토큰 전달 vs Token Exchange

서비스 A가 사용자 요청을 받아 서비스 B를 호출할 때 사용자 컨텍스트를 어떻게 넘길 것인가의 문제입니다.

**패턴 1: 원 토큰 그대로 전달 (token relay)**

Client --(JWT aud=api)--> Gateway --(같은 JWT)--> Service A --(같은 JWT)--> Service B

- 장점: 단순, 추가 IdP 왕복 없음.

- 단점: audience가 광역이어야 해서 토큰 탈취 시 모든 서비스가 위험. 토큰 수명 동안 B가 A를 가장해 다른 서비스 호출 가능. 위임 정보가 없어 감사가 불완전.

**패턴 2: Token Exchange ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693))**

서비스 A가 받은 토큰을 IdP에 제시하고, audience가 B로 좁혀진 새 토큰으로 교환합니다.

curl -s -X POST https://keycloak.example.com/realms/prod/protocol/openid-connect/token \

-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \

-d client_id=service-a \

-d client_secret=SERVICE_A_SECRET \

-d subject_token=ORIGINAL_USER_ACCESS_TOKEN \

-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \

-d audience=service-b

응답으로 받는 토큰에는 sub(원 사용자)와 함께 act(actor) 클레임이 들어가 "service-a가 사용자를 대신해 행동"한다는 위임 체인이 기록됩니다.

{

"iss": "https://keycloak.example.com/realms/prod",

"sub": "user-1234",

"aud": "service-b",

"scope": "orders:read",

"act": {

"sub": "service-account-service-a"

},

"exp": 1781234567

}

- 장점: audience 최소화, 위임 체인 보존, 권한 축소(scope down) 가능. AI agent 시대의 위임 추적에도 동일 메커니즘이 쓰입니다.

- 단점: 홉마다 IdP 왕복(캐싱 필수), IdP의 exchange 정책 관리 부담.

실무 절충안은 "신뢰 경계를 넘는 곳(도메인 간, 민감 서비스 진입)에서만 exchange하고, 같은 신뢰 경계 안에서는 relay + mTLS"입니다. 참고로 이 흐름을 표준화한 transaction tokens 논의가 OAuth WG에서 진행 중이며, 다음 글(SPIFFE/SPIRE)에서 워크로드 신원과 함께 다시 다룹니다.

Kong vs APISIX — OIDC 플러그인 관점 비교

Envoy 계열 밖에서 가장 많이 쓰이는 게이트웨이 두 종의 OIDC 처리 방식을 비교합니다.

| 항목 | Kong (openid-connect 플러그인) | APISIX (openid-connect / authz-keycloak) |

| --------------- | ------------------------------------- | ------------------------------------------ |

| 기반 | nginx/OpenResty + lua-resty-openidc | nginx/OpenResty + lua-resty-openidc |

| 라이선스 범위 | OIDC 플러그인은 Enterprise | OSS에 포함 |

| 동작 모드 | 검증(JWT), 세션(쿠키), relying party | 검증, relying party, Keycloak 인가 연동 |

| JWKS 캐싱 | 내장, 디스커버리 캐시 | 내장, 디스커버리 캐시 |

| 세밀한 인가 | ACL/스코프 플러그인 조합 | authz-keycloak로 UMA 권한 평가 위임 |

| 선언적 관리 | decK, Kong CRD | APISIX CRD, ADC |

APISIX 설정 예시는 다음과 같습니다.

apiVersion: apisix.apache.org/v2

kind: ApisixRoute

metadata:

name: orders-route

namespace: apps

spec:

http:

- name: orders

match:

hosts:

- api.example.com

paths:

- /orders/*

backends:

- serviceName: orders

servicePort: 8080

plugins:

- name: openid-connect

enable: true

config:

discovery: https://keycloak.example.com/realms/prod/.well-known/openid-configuration

client_id: apisix-gateway

client_secret: GATEWAY_CLIENT_SECRET

bearer_only: true

use_jwks: true

token_signing_alg_values_expected: RS256

audience: orders-api

bearer_only: true는 "브라우저 리다이렉트 없이 Bearer 토큰만 검증"하는 API 게이트웨이 모드입니다. 웹 앱 세션까지 게이트웨이가 처리하게 하려면 bearer_only를 끄고 relying party 모드로 씁니다(이 경우 게이트웨이가 IAP에 가까워집니다).

Gateway API 시대의 인증 표준화

쿠버네티스 [Gateway API](https://gateway-api.sigs.k8s.io/)는 Ingress의 후계자로 라우팅 표현은 표준화했지만, 인증/인가는 오랫동안 구현체별 확장(정책 CRD)의 영역이었습니다. 2025~2026년의 흐름은 다음과 같습니다.

- Gateway API 1.4에서 BackendTLSPolicy 등 정책 부착(Policy Attachment) 패턴이 자리 잡았고, 인증 필터의 표준화 논의(HTTPRoute 수준의 JWT/extAuth 필터)가 GEP(Gateway Enhancement Proposal)로 진행 중입니다.

- 그 전까지 실무는 구현체별 정책 CRD를 씁니다. Envoy Gateway의 SecurityPolicy가 대표적입니다.

apiVersion: gateway.envoyproxy.io/v1alpha1

kind: SecurityPolicy

metadata:

name: orders-jwt

namespace: apps

spec:

targetRefs:

- group: gateway.networking.k8s.io

kind: HTTPRoute

name: orders-route

jwt:

providers:

- name: keycloak

issuer: https://keycloak.example.com/realms/prod

audiences:

- orders-api

remoteJWKS:

uri: https://keycloak.example.com/realms/prod/protocol/openid-connect/certs

claimToHeaders:

- claim: sub

header: x-jwt-sub

같은 Envoy 기반이라도 Istio는 자체 CRD, Envoy Gateway는 SecurityPolicy, Gloo는 또 다른 CRD를 쓰는 파편화가 현재의 현실입니다. 다만 모두 밑단은 jwt_authn 필터이므로, 이 글 앞부분의 원리 이해가 모든 구현체에 통합니다. 장기적으로는 HTTPRoute에 인증 필터를 표준 문법으로 붙이는 방향이 유력합니다.

mTLS와 JWT의 조합 — 서비스 신원과 사용자 신원

mTLS와 JWT는 경쟁 관계가 아니라 서로 다른 질문에 답하는 직교적 수단입니다.

- **mTLS (peer identity)** — "이 요청을 보낸 워크로드는 누구인가". Istio에서는 PeerAuthentication으로 강제하고, 신원은 SPIFFE 형식의 principal로 표현됩니다.

- **JWT (request identity)** — "이 요청이 대변하는 최종 사용자는 누구인가".

둘을 조합한 AuthorizationPolicy가 Zero Trust 마이크로서비스의 표준형입니다.

apiVersion: security.istio.io/v1

kind: PeerAuthentication

metadata:

name: default

namespace: orders

spec:

mtls:

mode: STRICT

apiVersion: security.istio.io/v1

kind: AuthorizationPolicy

metadata:

name: orders-payment-call

namespace: payments

spec:

selector:

matchLabels:

app: payments

action: ALLOW

rules:

- from:

- source:

호출자 워크로드 제한 (mTLS 기반 서비스 신원)

principals: ['cluster.local/ns/orders/sa/orders-sa']

최종 사용자 토큰도 함께 요구

requestPrincipals: ['https://keycloak.example.com/realms/prod/*']

to:

- operation:

methods: ['POST']

paths: ['/payments']

when:

- key: request.auth.claims[scope]

values: ['*payments:write*']

이 정책은 "orders 서비스 계정의 워크로드가, 유효한 사용자 토큰과 payments:write 스코프를 가지고, POST /payments를 호출할 때만 허용"을 한 장에 표현합니다. 서비스 신원(mTLS)과 사용자 신원(JWT)의 이중 검증입니다.

성능 관점

JWT 검증 비용에 대한 일반적인 관찰을 정리합니다(절대 수치는 환경 의존이므로 직접 벤치마크 권장).

- RS256 서명 검증은 요청당 수십 마이크로초 수준으로, p50 지연에는 거의 영향이 없습니다. ES256/EdDSA는 검증이 더 가볍고 키도 짧아 2026년 신규 구축에서 선호됩니다(Keycloak 26.6은 EdDSA를 지원합니다).

- 진짜 비용은 서명 연산이 아니라 **JWKS fetch가 요청 경로에 끼어드는 순간**입니다. async_fetch와 캐시로 요청 경로에서 분리하는 것이 핵심입니다.

- 사이드카 2차 검증의 추가 지연은 일반적으로 홉당 1ms 미만으로, defense in depth의 가치 대비 수용 가능한 수준입니다.

- 토큰 크기는 간과되는 비용입니다. 그룹/권한을 모두 클레임에 담아 8KB를 넘기면 헤더 한도 초과로 4xx가 나는 사고가 흔합니다. 클레임은 식별자 위주로 얇게 유지하고, 세밀한 권한은 OpenFGA 같은 별도 인가 서비스에 위임하는 것이 추세입니다.

트러블슈팅 — 401 디버깅 플로우차트

게이트웨이 401의 원인을 체계적으로 좁히는 절차입니다.

+--------------------------+

| 401 발생 |

+-----------+--------------+

|

토큰이 요청에 실려 있는가? (헤더 확인)

|

+----------- 아니오 ---+--- 예 -----------+

| |

클라이언트/프록시가 Authorization jwt.io 등으로 토큰 디코드(검증 말고 관찰)

헤더를 누락/제거? (프록시 체인 점검) |

+------------------+------------------+

| | |

iss가 설정과 exp가 지났는가? aud가 설정과

정확히 일치? (클럭 스큐 포함) 일치하는가?

| | |

불일치: trailing 만료: 갱신 로직 불일치: audience

slash, http/https, 점검, NTP 확인 매핑 재설계

realm 경로 확인

|

모두 정상이면 → 키 검증 단계 의심

|

+------------------+-------------------+

| |

토큰 헤더의 kid가 JWKS에 게이트웨이가 JWKS를

존재하는가? (curl로 JWKS 확인) 가져올 수 있는가?

| |

없음: 키 롤테이션 직후 캐시 fetch 실패: 클러스터 정의,

문제 → 캐시 TTL/롤테이션 유예 DNS, egress 정책, TLS 신뢰

정책 점검 체인 점검

|

모두 정상인데 401 → RequestAuthentication은 통과하고

AuthorizationPolicy에서 거부(403일 수도)인지,

룰 매칭(경로/메서드/스코프)을 점검

함께 쓰는 진단 명령 모음입니다.

1) 토큰 페이로드 확인 (서명 검증 없이 디코드)

TOKEN=eyJhbGciOi...

echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

2) JWKS 직접 조회 — kid 목록 확인

curl -s https://keycloak.example.com/realms/prod/protocol/openid-connect/certs | jq '.keys[].kid'

3) Istio 설정이 Envoy에 반영됐는지 확인

istioctl proxy-config listener deploy/istio-ingressgateway -n istio-system -o json \

| jq '.. | select(.name? == "envoy.filters.http.jwt_authn")'

4) Envoy 액세스 로그에서 거부 사유 플래그 확인

kubectl logs deploy/istio-ingressgateway -n istio-system | grep -E '401|403' | tail -5

5) jwt_authn 통계로 어느 단계에서 막히는지 확인

kubectl exec deploy/istio-ingressgateway -n istio-system -- \

pilot-agent request GET stats | grep -E 'jwt_authn|jwks'

401과 403의 구분도 중요합니다. Istio 기준으로 401은 RequestAuthentication(토큰 자체의 문제), 403은 AuthorizationPolicy(토큰은 유효하나 권한 부족)에서 발생합니다. 디버깅의 첫 분기점으로 삼으세요.

마치며

API Gateway 계층의 OIDC 토큰 검증은 "엣지에서 거르고 서비스에서 다시 검증"이라는 원칙 위에, Envoy jwt_authn이라는 공통 기반으로 수렴했습니다. 정리하면 다음과 같습니다.

- RequestAuthentication은 차단하지 않습니다. AuthorizationPolicy까지가 한 세트입니다.

- 가용성은 JWKS 캐싱 설계에서 갈립니다. async fetch, TTL, 키 롤테이션 유예를 묶어 설계하세요.

- audience는 좁게, 토큰은 얇게, 신뢰 경계를 넘을 때는 token exchange를 검토하세요.

- mTLS(서비스 신원)와 JWT(사용자 신원)는 조합해야 Zero Trust가 완성됩니다.

다음 글에서는 이 글의 mTLS 쪽 절반, 즉 SPIFFE/SPIRE 기반 워크로드 아이덴티티를 깊게 다룹니다.

참고 자료

- [Envoy jwt_authn 필터 문서](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter)

- [Istio Security 개념 문서](https://istio.io/latest/docs/concepts/security/)

- [Istio RequestAuthentication 레퍼런스](https://istio.io/latest/docs/reference/config/security/request_authentication/)

- [Istio AuthorizationPolicy 레퍼런스](https://istio.io/latest/docs/reference/config/security/authorization-policy/)

- [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/)

- [Envoy Gateway SecurityPolicy 문서](https://gateway.envoyproxy.io/docs/tasks/security/jwt-authentication/)

- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)

- [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [RFC 8725 — JWT Best Current Practices](https://datatracker.ietf.org/doc/html/rfc8725)

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

- [OAuth 2.1 draft (draft-ietf-oauth-v2-1)](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)

- [Keycloak 공식 문서](https://www.keycloak.org/documentation)

- [Apache APISIX openid-connect 플러그인](https://apisix.apache.org/docs/apisix/plugins/openid-connect/)

- [Kong OpenID Connect 플러그인](https://docs.konghq.com/hub/kong-inc/openid-connect/)

- [OpenFGA 문서](https://openfga.dev/docs)

현재 단락 (1/377)

마이크로서비스 아키텍처에서 가장 자주 반복되는 보안 논쟁 중 하나는 "JWT 검증을 어디서 할 것인가"입니다. 게이트웨이에서 한 번만? 모든 서비스에서? 둘 다? 2026년 현재 ...

작성 글자: 0원문 글자: 14,130작성 단락: 0/377