토큰 취소(Token Revocation)는 엔드포인트가 아니다
요약
DPoP를 통한 토큰 탈취 방지와 토큰 취소(Revocation) 메커니즘의 차이점을 설명합니다. DPoP는 토큰의 휴대성을 제한하지만, 시스템 전체에서 권한을 즉시 중단하는 취소 프로세스와는 별개의 보안 축임을 강조합니다.
핵심 포인트
- DPoP는 토큰만 유출되었을 때 재전송 공격을 효과적으로 방지함
- 토큰과 개인 증명 키가 함께 탈취될 경우 DPoP의 방어 한계 발생
- 토큰 취소는 분산 시스템 전체에서 권한을 무효화하는 운영적 문제임
- 보안 설계 시 송신자 제한(Sender-constraining)과 취소(Revocation)를 구분해야 함
최근에 제가 수정해야 할 문장을 하나 썼습니다: “DPoP는 재전송 공격(replay)을 방지합니다. 대부분의 팀이 놓치는 간극은 토큰이 유출된 후의 취소 지연(revocation lag)입니다.” 뒷부분은 실제 문제를 지적하고 있습니다. 하지만 앞부분은 너무 느슨합니다. 단순히 토큰만 유출된 것이라면, 올바르게 구현된 DPoP 흐름은 도둑에게 토큰을 무용지물로 만들어야 합니다. 더 어려운 문제는 공격자가 토큰과 그 증명 키(proof key)를 모두 가지고 있고, 클라이언트 프로세스 내부에서 실행할 수 있거나, 장치를 제어하거나, 이미 활성 세션(live session)을 이용 중일 때 시작됩니다. 그 시점에서는 질문이 '누가 토큰을 보유하고 있는가'가 아니라, '전체 시스템이 얼마나 빨리 그 토큰의 권한을 인정하지 않도록 중단할 수 있는가'로 바뀝니다.
이러한 구분은 토큰 탈취(token theft)와 오래된 권한(stale authority)이 서로 다른 보안 축이기 때문에 중요합니다. 송신자 제한(Sender-constraining)은 자격 증명(credential)을 가지고 도망치기 어렵게 만듭니다. 취소(Revocation)는 자격 증명, 클라이언트 또는 근본적인 결정(decision)을 더 이상 신뢰할 수 없게 되었을 때 발생하는 일입니다. 한 메커니즘은 휴대성(portability)을 제한하고, 다른 하나는 분산 시스템(distributed system) 전체의 현실을 바꿔야 합니다.
토큰은 암호학적으로 유효하면서도 운영상으로는 권한이 없을 수 있습니다.
첫째, DPoP가 실제로 제공하는 것
베어러 토큰(bearer token)은 현금처럼 작동합니다. 누구든 그것을 제시하면 사용할 수 있습니다. 리소스 서버(resource server)는 서명(signature), 발행자(issuer), 대상(audience), 만료 시간(expiry), 범위(scope)를 검증하지만, 제시자가 토큰이 발급된 클라이언트인지 여부는 알지 못합니다. 로그, 브라우저, 프록시(proxy) 또는 텔레메트리 파이프라인(telemetry pipeline)에서 복사된 토큰은 다른 머신에서 재전송(replay)될 수 있습니다.
RFC 9449로 표준화된 DPoP는 이를 변경합니다. 클라이언트는 개인 키(private key)를 사용하여 각 요청에 대해 수명이 짧은 증명(proof)에 서명합니다. 이 증명은 요청을 HTTP 메서드, 대상 URI, 타임스탬프(timestamp), 고유 식별자(unique identifier), 그리고 액세스 토큰(access token)이 있는 경우 해당 토큰의 해시(hash)와 결합합니다. 권한 부여 서버(authorization server)는 해당 키의 공개 부분(public half)에 토큰을 결합합니다. 토큰만 훔친 도둑은 일치하는 증명을 생성할 수 없습니다.
이는 큰 개선이지만, 그 경계는 명확합니다:
- 액세스 토큰(Access token)만 있는 경우: DPoP가 실질적인 도움을 줍니다. 토큰 자체만으로는 더 이상 휴대(portable)가 불가능합니다.
- 토큰과 탈취된 증명(proof) 하나가 있는 경우: 증명은 특정 메서드(method)와 URI로 제한되며, 서버는 증명의 유효 기간을 제한하거나, 중복된
jti값을 거부하고, 논스(nonce)를 요구할 수 있습니다. - 토큰과 개인 증명 키(private proof key)가 있는 경우: 공격자가 클라이언트인 것처럼 나타낼 수 있습니다.
- 클라이언트 프로세스, 장치 또는 활성 세션이 있는 경우: 공격자가 키를 추출할 필요조차 없을 수 있습니다. 악성 코드가 정당한 서명 컨텍스트(signing context)에 요청하여 유효한 증명을 생성하게 할 수 있기 때문입니다.
RFC 9449는 이를 직접적으로 명시합니다: 만약 클라이언트 실행 컨텍스트(execution context) 내에서 적대적인 코드가 실행된다면, DPoP 보안은 더 이상 보장되지 않습니다. 내보낼 수 없는(non-exportable) 키는 여전히 오프라인 도난은 방지하지만, 프로세스 내부의 공격자가 클라이언트가 온라인 상태인 동안 키를 사용하는 것은 막지 못합니다. DPoP는 승인되지 않은 토큰의 휴대성(portability) 문제를 해결합니다. 하지만 실행 능력이 침해된 이후의 오래된 권한(stale authority) 문제는 해결하지 못합니다.
서명은 이력을 증명할 뿐, 현재의 의도를 증명하지 않는다
자체 포함형(Self-contained) JWT 액세스 토큰은 API가 로컬에서 이를 검증할 수 있기 때문에 매력적입니다. 권한 부여 서버(authorization server)로의 네트워크 왕복(round trip)이 필요 없습니다. 핫 패스(hot path)에서의 중앙 의존성도 없습니다. 낮은 지연 시간(low latency), 높은 가용성(high availability), 깔끔한 수평 확장(horizontal scaling)을 제공합니다.
그 트레이드오프(trade-off)는 _자체 포함형(self-contained)_이라는 단어 속에 숨겨져 있습니다. 서명은 신뢰할 수 있는 발행자(issuer)가 진술을 했으며 그 진술이 변경되지 않았음을 증명합니다. 하지만 발행자가 지금도 그 진술을 존중하기를 원하는지는 증명하지 않습니다. 만약 리소스 서버(resource server)가 JWT의 서명과 내장된 클레임(claims)만을 검증한다면, 계정이 비활성화되거나, 역할(role)이 제거되거나, 장치가 침해된 것으로 선언된 후에도 토큰은 exp 값에 도달할 때까지 유효할 수 있습니다.
이것이 바로 JWT 취소(revocation)가 암호학적 연산이 아닌 이유입니다. 토큰 자체가 형식이 잘못되게(malformed) 변할 필요는 없습니다. 집행 지점(enforcement point)이 토큰이 담고 있는 것보다 더 최신의 권한 상태(authorization state)를 필요로 하는 것입니다.
RFC 7009는 OAuth 토큰 취소 (token-revocation) 엔드포인트를 정의합니다. 또한 이 표준은 운영상의 격차(operational gap)를 인정합니다. 즉, 무효화 (invalidation) 과정에서 전파 지연 (propagation delay)이 발생할 수 있으며, 이 기간 동안 일부 서버는 취소 사실을 알고 있지만 다른 서버는 알지 못할 수 있습니다. HTTP 200 응답은 해당 엔드포인트가 오류를 보고하지 않고 요청을 처리했다는 사실만을 나타냅니다. RFC는 유효하지 않은 토큰에 대해서도 의도적으로 동일한 응답을 반환합니다. 이는 특정 토큰이 존재했다는 사실이나, 모든 리소스 서버 (resource server)가 권한 수락을 중단했다는 것을 증명하지 않습니다.
이로 인해 우리는 두 가지 별개의 이벤트를 얻게 됩니다:
- 취소 결정 (Revocation decision): 제어 평면 (control plane)이 토큰, 세션 (session), 키 (key), 장치 (device) 또는 권한 부여 (grant)를 더 이상 신뢰해서는 안 된다고 기록하는 것.
- 취소 집행 (Revocation enforcement): 대상이 되는 모든 게이트웨이 (gateway), API, 리전 (region), 캐시 (cache), 장기 연결 (long-lived connection) 및 워커 (worker)가 실제로 이를 거부하는 것.
보안은 이 두 이벤트 사이의 간격에 존재합니다.
수렴해야 하는 네 가지 평면 (planes)
"토큰이 취소되었다"라는 문장은 네 가지 서로 다른 시스템을 하나의 문장으로 압축한 것입니다. 이들을 분리하여 다루면 아키텍처 검토 (architecture reviews)와 사고 대응 (incidents) 모두 훨씬 명확해집니다:
- 결정 평면 (Decision plane): 사람, 정책 엔진 (policy engine) 또는 ID 제공자 (identity provider)가 토큰 (token), 권한 부여 (grant), 세션 (session), 디바이스 (device), 워크로드 (workload) 또는 키 (key)를 더 이상 신뢰할 수 없다고 결정합니다. 이곳에는 결정의 이유, 범위 (scope) 및 유효 시간 (effective time)이 존재합니다.
- 전파 평면 (Distribution plane): 변경된 결정은 이벤트 스트림 (event streams), 인트로스펙션 응답 (introspection responses), 복제된 거부 상태 (replicated deny state), 구성 저장소 (configuration stores) 또는 캐시 무효화 (cache invalidations)를 통해 전달됩니다. 이 평면은 모든 리전 (region)이 얼마나 빨리 진실을 파악할 수 있는지를 결정합니다.
- 강제 평면 (Enforcement plane): 게이트웨이 (gateways), API, 사이드카 (sidecars), 워커 (workers) 및 연결 핸들러 (connection handlers)가 해당 진실을 참조하여 다음 사용을 거부합니다. 이곳은 페일 오픈 (fail-open) 동작, 오래된 캐시 (stale caches) 및 일관되지 않은 정책 버전이 악용될 수 있는 지점입니다.
- 영향 평면 (Effects plane): 이미 허용된 작업이 중단되거나 격리됩니다. 대기 중인 작업 (queued jobs)이 취소되고, 스트림 (streams)이 닫히며, 위임된 자격 증명 (delegated credentials)이 무효화되고, 가능한 경우 되돌릴 수 없는 부작용 (side effects)을 방지합니다.
하나의 구현이 첫 번째 평면에서는 성공하고 나머지 세 평면에서는 실패할 수 있습니다. 데이터베이스 행 (database row)에는 revoked_at=now()라고 기록되어 있지만, 다른 리전에서는 캐시된 활성 응답을 제공하고, 이미 권한을 부여받은 WebSocket은 계속 명령을 전달하며, 워커가 권한이 있는 내보내기 (privileged export)를 완료할 수 있습니다. 엔드포인트 (endpoint)는 정상(healthy)이지만, 보안 결과 (security outcome)는 그렇지 않습니다.
이 모델은 또한 흔히 발생하는 범주 오류 (category error)를 방지합니다: 탐지 시간 (detection time)과 취소 시간 (revocation time)은 동일한 지표가 아닙니다. 만약 침해 (compromise)가 _t0_에 시작되어, 시스템이 _t1_에 이를 탐지하고, _t2_에 결정을 기록하며, 마지막으로 보호된 경로가 _t3_에 권한 수락을 중단한다면, 세 개의 별도 구간이 존재합니다:
- 탐지 지연 시간 (Detection latency): t1 − t0.
- 결정 지연 시간 (Decision latency): t2 − t1.
- 취소 완료 시간 (Revocation Completion Time): t3 − t2.
침해부터 거부까지의 전체 창 (compromise-to-denial window)은 _t3 − t0_입니다. 이벤트 전파 (event propagation)를 개선한다고 해서 한 시간의 탐지 지연 시간을 보상할 수는 없으며, 탐지가 빨라지더라도 강제 (enforcement)가 느리게 수렴한다면 도움이 되지 않습니다. 각 구간을 별도로 보고해야 하며, 그렇지 않으면 합계 수치가 실제로 어떤 통제 수단 (control)이 실패했는지를 가리게 될 것입니다.
취소(Revocation)는 분산된 수렴 보장(distributed convergence guarantee)이다
유용한 지표는 "취소 엔드포인트(revoke endpoint)가 80밀리초 이내에 응답했다"가 아닙니다. 저는 운영상의 측정 지표를 **취소 완료 시간 (Revocation Completion Time, RCT)**이라고 부릅니다. 이는 중앙의 취소 결정(central revocation decision) 시점부터 정책(policy)이 커버하는 마지막 실행 경로(execution path)가 해당 권한을 행사할 수 없음을 확인한 시점까지의 시간입니다.
약식으로 표현하면 다음과 같습니다:
RCT = 권한 거부가 확인된 마지막 커버된 집행 지점(enforcement point) − 중앙 취소 결정
그 마지막 집행 지점(enforcement point)이야말로 아키텍처가 위협 모델(threat model)이 되는 지점입니다. 그곳은 로컬 거부 캐시(local deny cache)를 가진 API 게이트웨이일 수도 있고, 서비스 메시 사이드카(service mesh sidecar), 제2의 리전(region), 파트너 API, 연결 시점에만 인증되는 WebSocket, 또는 취소 이벤트가 도착하기 전에 작업을 수락한 백그라운드 워커(background worker)일 수도 있습니다.
RCT는 선언된 커버리지 세트(coverage set)가 있을 때만 의미가 있습니다. "모든 집행 지점"은 인벤토리(inventory)가 아닙니다. 보장 범위 내에 있는 게이트웨이, 서비스, 리전, 연결 유형, 큐(queue) 및 위임된 시스템(delegated systems)의 이름을 명시하십시오. 그런 다음 시간의 함수로서 수렴(convergence)을 추적하십시오:
Coverage(t) = 시간 t에서 권한을 거부하는 커버된 집행 지점 ÷ 정책 범위 내의 모든 집행 지점
99%의 커버리지를 보여주는 대시보드라 할지라도, 누락된 1%가 결제 API라면 여전히 실패를 의미할 수 있습니다. 이러한 이유로, 완료(completion)는 전체 플릿(fleet)의 백분율로만 나타내서는 안 됩니다. 이를 위험 등급(risk class)별로 분할하고, 저위험 읽기(low-risk reads)에 대해 더 낮은 총합 임계값이 허용되더라도 핵심 경로(critical paths)에 대해서는 100%를 요구해야 합니다.
서비스 수준 목표(Service-level objective, SLO)는 최소한 네 가지 사항을 명시해야 합니다: 취소 대상(revocation subject), 커버리지 세트(coverage set), 백분위수(percentile), 그리고 작업 클래스(operation class)입니다. "모든 퍼스트 파티 리전(first-party region) 전반의 권한 있는 쓰기(privileged writes)에 대해 p99 RCT 5초 미만"은 테스트 가능합니다. 반면 "토큰이 빠르게 취소됨"은 마케팅 문구일 뿐입니다.
RFC 7662의 OAuth 토큰 인트로스펙션 (OAuth token introspection)은 최신 중앙 상태를 제공할 수 있습니다. 즉, 리소스 서버 (resource server)가 토큰이 활성 상태인지 묻는 방식입니다. 하지만 인트로스펙션 (introspection)은 네트워크 의존성을 추가하므로, 운영 시스템은 그 답변을 캐싱 (cache)합니다. 캐싱을 하는 순간, 캐시 TTL (Time To Live)은 보안 모델의 일부가 됩니다. 60초의 캐시는 단순한 성능 설정이 아닙니다. 그것은 취소된 결정이 최대 1분 동안 더 사용 가능하도록 허용하는 권한입니다.
푸시 (Push) 방식은 전송 (transport) 방식을 바꾸는 것이지, 보증 (guarantee)을 바꾸는 것이 아닙니다. OpenID CAEP 1.0은 세션 취소 (session revoked), 토큰 클레임 변경 (token claims change), 자격 증명 변경 (credential change), 디바이스 준수 상태 변경 (device compliance change), 리스크 수준 변경 (risk level change)과 같은 이벤트들을 표준화합니다. 이는 가치 있는 일입니다. 신뢰 당사자 (relying parties)가 모든 요청마다 폴링 (polling)을 하지 않고도 변화하는 리스크에 대응할 수 있기 때문입니다. 하지만 이벤트는 지연되거나, 중복되거나, 순서가 바뀌거나, 누락될 수 있습니다. 수신자가 오프라인일 수도 있습니다. 강제 적용 (Enforcement)이 실패하여 허용 상태로 남을 수도 있습니다 (fail open). 푸시 방식의 취소 (Push revocation) 역시 여전히 확인 (acknowledgement), 조정 (reconciliation), 그리고 테스트가 필요합니다.
토큰은 문제의 근원일 뿐이다
문자열 하나를 취소하는 것은 종종 가장 쉬운 부분입니다. 더 어려운 질문은 그 문자열이 이미 무엇을 만들어냈는가 하는 점입니다.
탈취된 세션은 리프레시 토큰 (refresh tokens)을 발행했을 수 있고, 액세스 토큰 (access token)을 위임된 토큰 (delegated token)으로 교환했을 수 있으며, API 키를 생성하거나, WebSocket을 열거나, 내보내기 (export)를 예약하거나, 백그라운드 워크플로 (background workflow)를 시작하거나, 새로운 관리자를 추가하거나, 사전 서명된 다운로드 URL (presigned download URL)을 발행했을 수 있습니다. 원래의 토큰을 취소한다고 해서 복사된 데이터가 삭제되거나, 실행 중인 작업이 취소되거나, 토큰이 유효한 동안 공격자가 생성한 영속성 (persistence)이 제거되지는 않습니다.
따라서 권한 (Authority)은 다음과 같은 그래프를 형성합니다:
세션 (session) → 리프레시 토큰 계열 (refresh-token family) → 액세스 토큰 (access tokens) → 위임된 자격 증명 (delegated credentials) → 작업 및 부수 효과 (jobs and side effects)
진정한 킬 스위치(kill switch)는 해당 그래프에 대한 정의된 클로저(closure)를 필요로 합니다. “이 에이전트를 취소(revoke)하라”는 명령이 하나의 액세스 토큰(access token)을 의미하는지, 하나의 실행(run), 하나의 디바이스(device), 해당 패밀리의 모든 토큰, 모든 서브에이전트(subagent), 모든 위임된 권한(delegated capability), 모든 활성 작업(active job), 아니면 해당 실행에 의해 생성된 모든 자격 증명(credentials)을 의미하는지 명확해야 합니다. 만약 답이 명시적이지 않다면, 엔드포인트(endpoint)는 구현된 대로 정확히 작동하더라도, 아무도 기억하지 못한 하위 엣지(child edge)를 통해 사고가 계속될 수 있습니다.
이는 현재 활발한 표준화 작업(standards work)이 되고 있습니다. 현재 진행 중인 에이전트 위임 체인을 위한 권한 축소 토큰에 관한 IETF 인터넷 초안(IETF Internet-Draft on attenuating authorization tokens for agentic delegation chains)은 각 홉(hop)마다 권한이 좁혀질 수만 있는 소유자 유도 토큰(holder-derived tokens)을 탐구합니다. 이는 초안(draft)이며 완성된 표준은 아니지만, 방향은 옳습니다. 에이전트의 권한에는 계보(lineage)가 있으며, 집행 지점(enforcement point)은 에이전트 런타임(runtime) 내부의 주변 자격 증명(ambient credentials)을 신뢰하기보다 전체 체인을 검증해야 합니다.
AI 에이전트는 초(seconds)를 행동으로 전환한다
인간의 세션(session)의 경우, 30초의 취소 지연은 두세 번의 의미 있는 행동을 허용할 수 있습니다. 하지만 에이전트는 동일한 시간 창(window) 내에서 수백 번의 도구 호출(tool calls)을 수행할 수 있으며, 서브에이전트를 생성하고 비동기적으로 작업을 계속할 수 있습니다. 신원 메커니즘(identity mechanism)이 약해진 것이 아닙니다. 실행 속도가 만료된 시간 창(stale window)의 비용을 배가시킨 것입니다.
이것이 바로 AI 에이전트 보안에서 “수명이 짧은 자격 증명(short-lived credentials)을 사용하라”는 말이 과소평가하고 있는 부분입니다. 시간 제한은 해당 시간 내의 행동 속도와 영향력 또한 제한될 때에만 노출을 줄일 수 있습니다.
저는 이 비용을 **취소 노출(Revocation Exposure)**이라고 부릅니다:
취소 노출 (Revocation Exposure) = 취소 완료 시간 (Revocation Completion Time) × 허용된 행동 속도 (accepted action rate) × 평균 되돌릴 수 없는 영향 (average irreversible impact)
만약 영향력(impact)을 정직하게 정규화(normalize)할 수 없다면, 마지막 항을 제외하고 _위험에 처한 행동(actions at risk)_을 보고하십시오. 그것은 단순한 TTL(Time-to-Live)을 제시하는 것보다 여전히 더 유용합니다. 초당 0.1회의 유의미한 행동(meaningful actions)이 발생한다고 가정할 때, 30초의 윈도우(window)는 대략 3개의 행동을 노출시킵니다. 초당 100회의 승인된 행동(accepted actions)이 발생한다면, 이는 3,000개의 행동을 노출합니다. 동일한 속도에서 15분 동안 유효한 액세스 토큰(access token)은 운영적인 관점에서 결코 "수명이 짧은(short-lived)" 것이 아닙니다. 그것은 90,000개의 행동이 발생할 여지를 포함하고 있습니다.
실제 사례 연구: 네 가지 아키텍처, 하나의 에이전트 (agent)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기