Skip to content
Published on

API Gateway에서의 OIDC 토큰 검증 — Istio, Envoy, Gateway API 실전

Authors

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

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

이 글에서는 Envoy의 jwt_authn 필터를 바닥부터 이해하고, Istio의 RequestAuthentication + AuthorizationPolicy 조합을 풍부한 YAML 예제로 살펴봅니다. JWKS 캐싱과 장애 모드, audience 전략, 토큰 전파 패턴(RFC 8693 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 실패 → 전면 401async_fetch + 캐시 TTL 여유, IdP 다중화
키 롤테이션 직후새 kid 토큰이 캐시 미스로 401IdP는 새 키 공개 후 유예기간을 두고 서명 전환
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)

서비스 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-openidcnginx/OpenResty + lua-resty-openidc
라이선스 범위OIDC 플러그인은 EnterpriseOSS에 포함
동작 모드검증(JWT), 세션(쿠키), relying party검증, relying party, Keycloak 인가 연동
JWKS 캐싱내장, 디스커버리 캐시내장, 디스커버리 캐시
세밀한 인가ACL/스코프 플러그인 조합authz-keycloak로 UMA 권한 평가 위임
선언적 관리decK, Kong CRDAPISIX 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는 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 기반 워크로드 아이덴티티를 깊게 다룹니다.

참고 자료