key rotation 직후 일부 JWT만 unknown kid 또는 invalid signature로 실패하면 저는 가장 먼저 새 key가 언제 publish됐고, issuer가 언제부터 그 key로 서명했는지를 시간순으로 봅니다. 안전한 rotation은 “key를 바꾼다” 한 단계가 아니라 publish, cache, signing, overlap, retirement가 연결된 절차입니다.
제가 planned rotation을 운영하는 기본 순서
제가 기대하는 순서는 다음과 같습니다.
1. new key pair 생성
2. new public JWK를 JWKS에 publish
3. verifier/cache가 새 JWKS를 볼 시간을 줌
4. issuer가 new private key로 서명 시작
5. old + new public JWK를 일정 기간 함께 유지
6. old token 최대 수명이 지난 뒤 old public key 제거
7. old private key 폐기
핵심은 sign first가 아니라 publish first입니다.
왜 new JWK를 먼저 publish하는가
issuer가 new kid로 token을 만들었는데 verifier의 JWKS에 아직 그 key가 없다면 새 token은 즉시 실패합니다.
new token
header.kid = key-B
cached JWKS
keys = [key-A]
→ unknown kid
그래서 저는 JWKS propagation과 cache를 고려한 뒤 signing key를 전환합니다.
Old key를 바로 제거하지 않는 이유
rotation 순간에도 old key로 서명된 token이 아직 유효할 수 있습니다.
예를 들어 access token 최대 lifetime이 1시간이면 rotation 직전 발급된 old token은 최대 1시간 더 살아 있을 수 있습니다.
따라서 old public key retirement 시점은 대략 다음 요소를 고려합니다.
old key retirement >=
old token 최대 lifetime
+ JWKS/cache 여유
+ 운영 safety margin
정확한 값은 시스템 정책에 따라 달라집니다.
kid가 rotation을 단순하게 만드는 방식
각 signing key에 고유한 kid를 사용하면 token header가 trusted JWKS 안에서 어떤 key를 찾아야 하는지 알려줍니다.
{
"alg": "RS256",
"kid": "2026-09-key-b"
}
JWKS에는 동시에:
{
"keys": [
{ "kid": "2026-08-key-a", "kty": "RSA", "...": "..." },
{ "kid": "2026-09-key-b", "kty": "RSA", "...": "..." }
]
}
처럼 old/new key가 존재할 수 있습니다.
kid는 trust 자체가 아니라 key selection identifier입니다. JWT kid 가이드에서 이 경계를 더 자세히 설명합니다.
Unknown kid가 왔을 때 cache를 어떻게 보는가
verifier가 JWKS를 매 요청마다 fetch할 필요는 없습니다. cache는 성능과 availability에 도움이 됩니다.
하지만 새 kid가 왔을 때 cache TTL이 끝날 때까지 무조건 기다리면 rotation 중 장애가 발생할 수 있습니다.
제가 선호하는 패턴은:
- cached JWKS에서
kid검색 - 없으면 rate-limit된 JWKS refresh 시도
- 새 set에서 다시 검색
- 그래도 없으면 fail closed
입니다.
공격자가 random kid로 refresh storm을 만들 수 있으므로 refresh에는 rate limit/coalescing이 필요합니다.
Cache-Control도 확인합니다
provider가 JWKS endpoint에 cache header를 제공할 수 있습니다. 저는 application cache TTL만 보지 않고 CDN, reverse proxy, SDK 내부 cache까지 확인합니다.
문제가 “일부 인스턴스에서만” 발생한다면 각 verifier instance가 서로 다른 JWKS snapshot을 가지고 있을 수 있습니다.
Planned rollover와 emergency revocation은 다릅니다
이 둘을 같은 절차로 보면 안 됩니다.
Planned rotation
목표는 정상 token을 끊지 않고 key를 교체하는 것입니다. old/new overlap이 중요합니다.
Emergency revocation
private key 유출이 의심되면 old key를 계속 유지하는 것이 더 위험할 수 있습니다. 이 경우 아직 lifetime이 남은 old token도 의도적으로 invalid하게 만드는 결정을 할 수 있습니다.
즉 availability보다 compromise containment가 우선될 수 있습니다.
Private key 유출 시 제가 보는 것
emergency 상황에서는:
- compromised key로 서명된 token scope
- token 최대 lifetime
- 해당 key가 사용된 issuer/tenant
- JWKS에서 key 제거 시점
- verifier cache purge 방법
- replacement key 배포
- session/token revocation 정책
- incident 로그와 audit
를 함께 봅니다.
단순히 kid 하나를 삭제하는 것으로 끝나지 않을 수 있습니다.
같은 kid를 재사용하지 않는 이유
key material을 바꾸면서 같은 kid를 계속 쓰면 cache가 old key를 유지하는 동안 혼란이 생길 수 있습니다.
저는 일반적으로 new key material에는 new kid를 사용합니다. 그러면 verifier가 cache에서 old/new key를 명확히 구분할 수 있습니다.
JWK Inspector의 thumbprint는 실제 public key material이 같은지 비교하는 데 도움이 됩니다.
Rotation 후 invalid signature가 나면
제가 확인하는 순서는:
- failing token의
kid확인 - live JWKS에 그
kid가 있는지 확인 - failing verifier가 보는 cached JWKS 확인
- candidate JWK의
kty,alg,use,key_ops확인 - 실제 signature verify
- issuer가 올바른 private key로 서명했는지 확인
JWKS Validator를 사용하면 token과 현재 JWKS 조합을 브라우저에서 재현할 수 있습니다.
여러 region/cluster에서의 rotation
multi-region issuer에서는 더 복잡합니다.
예를 들어 region A는 new key로 서명하고 region B는 아직 old key를 쓰는 짧은 transition이 있을 수 있습니다. 이때 JWKS는 두 key를 모두 제공해야 합니다.
저는 deployment 순서를 다음처럼 맞춥니다.
JWKS global publish
→ cache propagation 확인
→ issuer regions signing transition
→ old token drain
→ old key retirement
Token lifetime이 rotation 비용을 결정합니다
access token lifetime이 매우 길면 old public key를 더 오래 유지해야 합니다. 반대로 짧은 lifetime은 key retirement를 빠르게 할 수 있지만 refresh/load와 UX에 영향을 줍니다.
그래서 key rotation 정책과 token lifetime 정책을 따로 설계하지 않습니다.
JWT exp Claim 가이드에서 exp, iat와 lifetime을 계산하는 방법을 설명합니다.
제가 rotation 전 테스트하는 시나리오
production 전 최소 다음을 재현합니다.
old token + JWKS(old,new) → pass
new token + JWKS(old,new) → pass
new token + stale JWKS(old only) → refresh 후 pass
unknown random kid → controlled refresh 후 fail
old token + new-only JWKS → retirement 시점 이후에만 fail
그리고 cache가 실제로 어떤 조건에서 refresh되는지 확인합니다.
제가 피하는 운영 패턴
- new public key publish 전에 signing 전환
- key material 변경하면서 같은
kid재사용 - unknown kid마다 무제한 JWKS fetch
- old key를 token lifetime 고려 없이 즉시 삭제
- compromised key를 planned rotation처럼 오래 overlap
- token header의
jku를 rotation key source로 무조건 사용
제 key rotation 체크리스트
- new key에 새로운
kid를 부여합니다. - new public JWK를 먼저 publish합니다.
- JWKS/CDN/verifier cache propagation을 확인합니다.
- 그 다음 issuer signing key를 전환합니다.
- old/new key를 token lifetime 동안 overlap합니다.
- unknown
kidrefresh를 rate-limit해서 구현합니다. - old token drain 후 old public/private key를 retirement합니다.
- emergency revocation 절차는 planned rollover와 별도로 정의합니다.
- old/new/stale-cache 테스트를 자동화합니다.
제가 rotation을 성공했다고 판단하는 기준은 “새 token이 발급됐다”가 아닙니다. rotation 전후에 old token과 new token이 의도한 기간 동안 정확히 검증되고, cache와 key retirement가 그 정책을 따라가는가입니다.