Published on

Kong OIDC JWT 401 - JWKS 캐시·키회전 대응

Authors

Kong을 API Gateway로 두고 OIDC 기반 JWT 검증을 붙이면, 평소엔 잘 되다가도 특정 시점에 갑자기 401이 쏟아지는 경우가 있습니다. 특히 IdP가 키를 회전(rotation)하는 환경(예: 매일/매주 자동 롤오버, 장애 시 강제 재발급)에서 자주 보입니다.

이 글은 다음 상황을 전제로 합니다.

  • 클라이언트는 정상적으로 Access Token을 발급받고 있음
  • 토큰의 kid가 바뀌는 순간(키 회전 직후)부터 Kong이 검증에 실패
  • 잠시 후(캐시 만료 혹은 Kong 재시작 후) 다시 정상화

핵심은 Kong이 JWKS를 어떻게 캐시하고, 키 회전 순간에 어떤 불일치가 생기는지를 이해한 다음, 캐시 정책/타임아웃/프리패치로 401 구간을 없애는 것입니다.

관련해서 OAuth 계열 401의 다른 흔한 원인(state, PKCE 등)도 함께 점검하면 좋습니다: NextAuth.js OAuth 401 - state·PKCE 오류 해결

401이 터지는 전형적인 패턴

증상은 보통 아래 중 하나로 나타납니다.

  • JWT 헤더의 kid에 해당하는 공개키를 JWKS에서 찾지 못해 검증 실패
  • JWKS 엔드포인트 호출 실패(일시적 네트워크/타임아웃/레이트리밋)로 캐시 갱신 실패
  • IdP는 새 키로 서명하기 시작했지만, JWKS는 아직 이전 버전이거나 반대로 JWKS는 갱신됐지만 토큰은 이전 키로 서명(전파 지연)

이때 Kong 쪽 로그에는 보통 다음 류의 메시지가 섞입니다(플러그인/버전에 따라 표현은 다르지만 의미는 유사합니다).

  • no matching JWK for kid
  • failed to fetch jwks
  • invalid signature
  • could not verify token

중요한 점은, 이 문제는 “토큰 자체가 잘못됨”이 아니라 토큰과 JWKS가 만나는 타이밍 문제인 경우가 많다는 것입니다.

JWKS 캐시와 키 회전이 충돌하는 구조

OIDC에서 JWT(보통 RS256/ES256)는 IdP의 개인키로 서명되고, 리소스 서버(Kong 포함)는 JWKS(JSON Web Key Set)에서 공개키를 내려받아 서명을 검증합니다.

키 회전 시나리오는 대략 이렇게 흘러갑니다.

  1. IdP가 새 키 쌍을 생성하고, 새 kid를 가진 공개키를 JWKS에 추가
  2. 일정 시간 동안 “구 키 + 신 키”를 동시에 JWKS에 노출(그레이스 기간)
  3. 토큰 발급은 신 키로 전환
  4. 충분한 시간이 지난 뒤 구 키를 JWKS에서 제거

여기서 401이 생기는 구간은 보통 23 또는 34 사이입니다.

  • Kong이 JWKS를 캐시한 상태에서, IdP가 토큰 서명 키를 바꿨는데 Kong 캐시가 아직 갱신되지 않음
  • 혹은 Kong이 JWKS를 갱신했는데, 클라이언트/다른 컴포넌트가 아직 구 키로 서명된 토큰을 들고 옴(토큰 TTL이 길수록 가능성 증가)

즉, JWKS 캐시 TTL과 토큰 TTL, 그리고 IdP의 키 제거 시점이 서로 맞지 않으면 401이 발생합니다.

먼저 확인할 체크리스트(재현 없이도 가능)

1) 실패한 토큰의 kid와 현재 JWKS의 kid 비교

실패한 요청에서 토큰을 추출해 헤더를 확인합니다.

TOKEN="eyJ..."
python3 - << 'PY'
import base64, json, os

t = os.environ['TOKEN']
header_b64 = t.split('.')[0]
# base64url padding
pad = '=' * (-len(header_b64) % 4)
h = base64.urlsafe_b64decode(header_b64 + pad)
print(json.dumps(json.loads(h), indent=2))
PY

출력에서 kid를 확인한 뒤, JWKS 엔드포인트에서 현재 키 목록을 확인합니다.

JWKS_URL="https://idp.example.com/.well-known/jwks.json"
curl -s "$JWKS_URL" | jq '.keys[].kid'
  • 토큰의 kid가 JWKS에 없다면, 401은 거의 확정적으로 키 회전/전파 지연/캐시 이슈입니다.

2) Kong이 JWKS를 갱신하는 타이밍/실패 여부 확인

Kong의 에러 로그를 먼저 봅니다.

  • Docker라면 docker logs
  • K8s라면 kubectl logs

예시:

kubectl logs -n kong deploy/kong -c proxy --since=30m | grep -i jwk

로그에서 fetch 실패, 타임아웃, DNS 오류가 보이면 캐시 만료 시점에 갱신이 실패하면서 401이 길게 이어질 수 있습니다.

인프라 레벨에서 간헐 장애가 있는지도 함께 보세요. (예: 노드 리소스 부족, Pod Pending, 네트워크 지연 등) 필요하면 EKS Pod Pending - Insufficient cpu·taint 해결 같은 관점으로도 확인할 가치가 있습니다.

3) 토큰 TTL과 IdP 키 제거 정책 확인

  • Access Token TTL이 1시간인데 IdP가 회전 직후 구 키를 5분 만에 제거한다면, 55분 동안 합법 토큰이 401이 됩니다.
  • 반대로 JWKS 캐시 TTL이 너무 길면, 새 키로 서명된 토큰이 들어오는 순간부터 캐시가 갱신될 때까지 401이 납니다.

결론적으로 **구 키를 JWKS에 유지하는 기간은 최소한 “최대 토큰 TTL + 전파 지연 여유분”**이어야 합니다.

Kong에서 특히 자주 생기는 실수 포인트

Kong에서 JWT 검증은 구성에 따라 다음 플러그인/컴포넌트로 이뤄질 수 있습니다.

  • OIDC 플러그인(외부 플러그인 포함)
  • OpenID Connect 플러그인(엔터프라이즈/버전별 제공)
  • JWT 플러그인 + 별도 JWKS fetch 로직(커스텀)

플러그인마다 JWKS 캐시 키, TTL, 갱신 방식(요청 시 갱신 vs 백그라운드 갱신)이 다릅니다. 하지만 공통적으로 아래가 문제를 키웁니다.

  • JWKS 캐시 TTL이 토큰 TTL보다 훨씬 김
  • 캐시 미스 시 동시 요청이 몰려 JWKS 엔드포인트에 스파이크
  • JWKS fetch 실패 시 이전 캐시를 유지하지 못하고 즉시 실패(구현에 따라 다름)
  • Kong 노드가 여러 대일 때 캐시가 노드 로컬이라 노드별로 다른 시점에 401

해결 전략 1: IdP 키 회전 정책부터 “안전하게” 만들기

가장 확실한 해결은 IdP 정책 조정입니다.

권장 정책

  • 새 키를 JWKS에 먼저 추가한 뒤, 일정 시간(예: 1일) 동안 구 키와 공존
  • 토큰 발급을 신 키로 전환
  • 구 키 제거는 “최대 토큰 TTL + 여유분” 이후에 수행

예시 계산:

  • Access Token TTL: 60분
  • 최대 클럭 스큐/전파 지연: 10분

그러면 구 키는 최소 70분 이상 JWKS에 남겨야 합니다.

만약 Refresh Token으로 Access Token을 재발급하는 구조라면, 실제로는 “Access Token TTL”만 고려하면 되는 경우가 많지만, 클라이언트가 오래된 Access Token을 들고 재시도하는 패턴(모바일 백그라운드, 큐잉 등)이 있다면 여유를 더 잡는 게 안전합니다.

해결 전략 2: Kong의 JWKS 캐시 TTL을 토큰 TTL과 정렬

실무적으로는 아래 원칙이 안전합니다.

  • JWKS 캐시 TTL은 너무 길게 잡지 말 것
  • 단, 너무 짧으면 JWKS 엔드포인트에 부하가 커짐

권장 범위(경험칙):

  • 회전이 드문 환경: 5분~30분
  • 회전이 잦거나 전파 지연이 있는 환경: 1분~5분 + 프리패치/백오프

플러그인별 설정 키는 다르므로 문서를 확인해야 하지만, 개념적으로는 “JWKS를 캐시하되, 회전 이벤트를 따라잡을 정도로 자주 갱신”이 목표입니다.

해결 전략 3: 캐시 미스/갱신 실패 시 동작을 설계(서킷 브레이커 관점)

401 폭발은 대개 다음 두 가지가 겹칠 때 커집니다.

  • 회전으로 인해 새 kid가 등장
  • 동시에 JWKS fetch가 실패하거나 느려서 캐시가 갱신되지 않음

이때는 아래 옵션을 검토하세요.

  • JWKS fetch 타임아웃을 너무 짧게 두지 않기
  • DNS/egress 정책 때문에 IdP JWKS에 접근이 막히지 않는지 확인
  • 갱신 실패 시 직전 캐시를 일정 시간 더 유지하는 전략(가능한 구현이라면)

또한 캐시 갱신 시점에 트래픽이 몰리는 구조라면, 레이트리밋/백오프가 필요합니다. (원리는 API 레이트리밋 대응과 동일합니다.)

해결 전략 4: 키 회전 직후 401을 없애는 “프리패치”

키 회전은 보통 일정합니다(예: 매일 00시). 그러면 Kong 앞단/운영 배치로 JWKS를 미리 당겨 캐시를 워밍업할 수 있습니다.

가장 단순한 방식은 “Kong이 아니라” Kong이 접근하는 네트워크 경로에서 JWKS를 미리 조회해 DNS/라우팅/방화벽/프록시 문제를 조기 발견하는 것입니다.

예시 크론잡:

#!/usr/bin/env bash
set -euo pipefail

JWKS_URL="https://idp.example.com/.well-known/jwks.json"

# 단순 워밍업 및 가용성 체크
curl -fsS "$JWKS_URL" | jq -e '.keys | length > 0' >/dev/null

K8s라면 CronJob으로, VM이라면 systemd timer로 돌리면 됩니다. systemd 기반으로 주기 작업/서비스가 불안정하다면 재시작 루프를 먼저 잡아야 합니다: systemd 서비스가 자꾸 재시작될 때 진단 방법

실전 디버깅: “정상 토큰인데 401”을 증명하는 절차

운영에서 가장 곤란한 건 보안팀/플랫폼팀/서비스팀 사이에서 “토큰이 문제다 vs 게이트웨이가 문제다” 공방이 길어지는 것입니다. 아래 절차로 빠르게 결론을 낼 수 있습니다.

1) 실패 시점의 토큰을 보관하고, 서명을 로컬에서 검증

JWKS를 내려받아, 해당 kid의 공개키로 로컬 검증을 해봅니다.

Node.js 예시(라이브러리 jose 사용):

npm i jose
import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwksUrl = new URL('https://idp.example.com/.well-known/jwks.json');
const JWKS = createRemoteJWKSet(jwksUrl);

const token = process.env.TOKEN;
const issuer = 'https://idp.example.com/';
const audience = 'your-audience';

const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
  issuer,
  audience,
});

console.log('kid:', protectedHeader.kid);
console.log('sub:', payload.sub);
  • 로컬 검증이 성공하는데 Kong만 401이면, Kong의 캐시/네트워크/플러그인 설정 문제일 확률이 큽니다.
  • 로컬 검증도 실패하면, 토큰 발급/클레임/issuer, audience 불일치 가능성을 의심합니다.

2) kid 불일치라면 “키 제거가 너무 빠른지” 확인

IdP 운영팀에 아래 두 정보를 요청하면 대화가 빨라집니다.

  • 회전 시각(새 키 활성화 시각)
  • 구 키가 JWKS에서 제거된 시각

그리고 Access Token TTL과 비교해 “구 키 유지 시간이 TTL보다 짧다”를 수치로 보여주면 됩니다.

운영 팁: 멀티 노드 Kong에서 401이 한쪽만 나는 이유

Kong이 여러 인스턴스로 떠 있고, JWKS 캐시가 노드 로컬이면 다음이 가능합니다.

  • A 노드: 캐시 갱신 완료, 정상
  • B 노드: 캐시 갱신 실패(일시적 네트워크), 401

이 경우 로드밸런서가 라운드로빈이면 “간헐적 401”처럼 보입니다.

대응은 두 갈래입니다.

  • 캐시를 공유할 수 있는 구조(플러그인/아키텍처에 따라 가능 여부가 다름)
  • 각 노드가 JWKS를 안정적으로 가져오도록 네트워크/리졸버/타임아웃을 강화

또한 배포 직후에만 401이 튄다면, 스타트업 시점에 DNS/egress가 아직 안정화되지 않았을 수 있습니다.

결론: 401을 없애는 최소 조건

키 회전 환경에서 Kong의 OIDC JWT 401을 안정적으로 없애려면 아래 3가지를 동시에 만족해야 합니다.

  1. IdP는 구 키를 충분히 오래 JWKS에 유지한다(최소 max(access_token_ttl) + 여유분)
  2. Kong의 JWKS 캐시 TTL은 회전을 따라잡을 만큼 짧게, 하지만 과도한 fetch를 유발하지 않게 조정한다
  3. JWKS fetch 실패를 운영 이슈로 다룬다(DNS/egress/타임아웃/레이트리밋/노드별 편차)

이 3가지가 맞춰지면, “정상 토큰인데 401” 같은 가장 난감한 장애를 대부분 제거할 수 있습니다.