How to Shrink Key Revocation Latency: Webhook Intake with a Scheduled Polling Backstop
키 폐기 지연 줄이기 — 웹훅 인테이크와 정기 폴링 백스톱의 결합
테넌트별 스코프드 키를 쓰는 시스템에서 키 폐기(revocation) 전파를 웹훅(속도)과 정기 폴링 스윕(검증)의 이중 채널로 설계하는 방법을 다룹니다. 신뢰성 소유권은 수신 측에 있다는 전제 아래 append-only 감사 로그 우선 기록, HMAC 서명 검증, 그리고 스윕의 divergence 카운트로 푸시 경로의 누락을 측정합니다. 테넌트 4,000개·5분 증분 스윕 기준 하루 1,000회 미만의 호출로 검증 가능성을 확보할 수 있다고 계산합니다.
- 주제
본문
API 키 폐기 전파 지연을 줄이는 아키텍처를 다룹니다. 발행자가 보내는 웹훅을 빠른 푸시 채널로 쓰되, 정기 폴링 스윕을 백스톱으로 두어 폐기가 실제로 적용됐는지 한 시간 뒤에라도 검증하는 구조입니다. 테넌트마다 스코프드 키를 하나씩 보유하는 고객 지원 데스크 제품을 예로 들며, 아키텍처를 결정하는 축은 중앙값 지연이 아니라 '한 키가 어느 오후에 접근이 끊겼고 우리가 그것을 어떻게 알았는지'를 재구성할 수 있는가라고 말합니다.
신뢰성 소유권은 수신 측에 있다
웹훅 발신자는 자신이 정한 재시도 예산 안에서만 재전송합니다. 예산이 소진되면 이벤트는 폐기되고, 그 사실을 알아챌 수 있는 유일한 쪽은 수신 시스템입니다. 즉 신뢰성 책임은 지원 계약이 아니라 전송 시맨틱스에서 자연스럽게 수신자에게 귀속됩니다. 에지 게이트웨이가 로컬 키 테이블을 유지해 요청당 네트워크 홉을 아끼는 구조에서, 이 테이블은 '남의 진실의 캐시'이며 설계 문제는 그 캐시가 얼마나 빠르고 검증 가능하게 'revoked'라는 단어를 배우는가입니다.
감사자가 요구하는 세 가지
접근 검토자들이 중요하게 보는 순서는 — 폐기가 결국 적용된다, 적용된 순간을 특정할 수 있다, 어떻게 알게 됐는지 말할 수 있다 — 이고 지연은 네 번째입니다. '오프보딩 티켓이 닫힌 뒤에도 이 키가 사용 가능했는가'라는 감사 질문은 백분위가 아니라 한 행의 레코드를 요구합니다. 그래서 인테이크는 append-only 로그가 먼저이고 캐시 갱신은 그다음이어야 하며, 순서를 뒤집으면 감사 추적은 현재 캐시가 우연히 담고 있는 것에 불과해집니다.
두 채널의 침묵 모드
등록된 웹훅은 발신자 소유 재시도 예산을 가진 at-least-once 전송으로, 낮은 지연과 자연스러운 멱등 키를 주지만 마지막 재시도 이후에는 '아무 일도 없었다'와 '인테이크가 503을 반환하는 동안 폐기 네 건이 버려졌다'를 구분할 수 없습니다. 정기 폴링은 이를 반전시켜 생존성(liveness)이 수신 측 것이 되고 지연은 폴 간격이 되며, 비용은 실제 이벤트가 아니라 테넌트 수 × 빈도에 비례합니다. 웹훅+정합성 스윕 조합에서는 푸시가 지연을, 스윕이 보장을 결정하고, 스윕의 divergence 카운트가 푸시 경로가 놓치는 양의 살아 있는 측정치가 됩니다.
서명 검증과 인테이크 구현
검증되지 않은 폐기 피드는 공격자가 테넌트 접근을 끄는 수단이 되므로 서명 검증이 출발점입니다. Standard Webhooks의 형식(webhook-id, webhook-timestamp, webhook-signature)과 id·타임스탬프·원문 바디에 대한 HMAC-SHA256을 쓰고, 양 끝단을 통제한다면 서명 헤더를 쓰는 RFC 9421이 표준 궤도의 대안입니다. 코드에서 주목할 세부는 두 가지입니다. 타임스탬프 창(MAX_SKEW_SECONDS = 300)이 탈취된 요청의 리플레이 기회를 영구적이 아닌 300초로 제한하고, eventId의 유니크 인덱스가 재전송을 202로 깨끗이 응답하게 해줍니다. 재전송은 엣지 케이스가 아니라 인테이크가 커밋 후 타임아웃됐을 때의 정상적 결과입니다. 핸들러는 1~2초 안에 반환해야 하며, 그 이상은 발신자의 재시도 예산에서 빌려오는 것이므로 핸들러 뒤의 큐가 핸들러 앞의 재시도 폭풍보다 쌉니다.
스윕과 비용 계산
스윕은 의도적으로 단순하게 설계합니다. updated_since 하이워터마크 기반 증분 조회에 커서 페이지네이션, 429 응답 시 retry-after 대기를 적용하고, 원격과 로컬 상태가 다를 때만 감사 레코드(eventId는 'sweep:{key_id}:{updated_at}')와 캐시를 갱신합니다. divergences는 대시보드에 올리고 알림을 걸 숫자로, 일주일간 0이면 푸시 경로가 건강하고 스윕은 순수 보험이며, 꾸준한 유출이면 푸시가 거짓말을 하고 있다는 뜻입니다. 비용의 계산 예시로, 테넌트 4,000개·5분 주기·페이지당 500키 조건에서 대부분의 스윕은 거의 빈 페이지 하나를 반환해 하루 약 288회 요청에 실제 변경 몇 페이지를 더해 1,000회 미만입니다. 목록 엔드포인트가 ETag를 지원하면 If-None-Match로 조용한 스윕을 바디 없는 304로 줄일 수 있습니다. 비싼 실수는 폴링 자체가 아니라 케이던스와 범위로, 전체 상태의 1분 스윕은 5분 증분 스윕보다 트래픽이 두 자릿수 크고 웹훅이 이미 확보한 지연을 다시 사는 셈입니다.
대안과 적용 범위
발행 플랫폼이 키 상태 아웃바운드 이벤트를 지원하지 않으면 폴링이 유일한 선택지이며, 지연은 접근 정책이 허용하는 간격으로 정해 런북에 명시해야 합니다 — 5분 폴을 '실시간'으로 포장하는 것이 사람들을 놀라게 하는 방식입니다. 게이트웨이가 요청당 한 번의 introspection 호출(RFC 7662 방식이나 짧은 TTL의 캐시된 상태 확인)을 감당할 수 있다면 전파 문제 자체가 사라지고 두 채널 모두 선택 사항이 됩니다. 수명 5분짜리 단기 토큰은 폐기를 만료로 바꿔 전파를 완전히 우회하며, 신규 구축이라면 저자가 권하는 방향이지만 정적 키에 자동화를 붙인 기존 테넌트에는 개조가 어렵습니다. 테넌트 20개 규모의 내부 도구는 단일 채널과 야간 정합성으로 충분하고, 약 10만 개를 넘으면 스윕당 목록 조회가 비싸져 전체 diff 대신 시퀀스 번호 하나를 쓰는 changelog/cursor 피드가 필요합니다.
하나만 가져간다면 이것입니다. 모든 상태 변경에 그 출처(push인지 sweep인지)와 관측 시각을 함께 기록하라는 것입니다. 누군가 '어떻게 알았는가'를 묻는 날, 그 컬럼이 답이며 어떤 지연 튜닝도 그것을 대신하지 못합니다.
dev.to 반응
아직 등록된 댓글이 없습니다.
원문: dev.to / 번역·요약: Trawling