Skip to content

Webhook 서명 키 회전

운영자 전용 페이지

이 페이지는 logi 운영자 (key rotation rake 태스크를 직접 실행하는 인프라/보안 담당) 를 위한 것입니다.

RP 통합 개발자는 다음 4가지만 알면 충분합니다:

  • logi 가 webhook signing key 를 회전합니다 (정기 또는 노출 시 즉시).
  • RP 는 GET /api/v1/webhook_signing_keys 로 active 키 목록을 주기적으로 풀합니다.
  • 다수 키를 동시에 캐시하고, 수신 webhook 의 kid 로 lookup 하세요.
  • kid miss 시 1회 강제 refetch.

자세한 RP 측 구현은 Webhook HMAC 서명 검증 을 참고하세요. 아래 운영 절차(rake 태스크, compromise flow 등)는 RP 측 구현에 필요하지 않습니다.

logi 의 webhook_signing_keys 테이블은 RP 1개당 다수의 활성 키를 동시에 들고 있을 수 있게 설계되어 있습니다 (zero-downtime rotation 및 노출 대응용). 이 페이지는 키를 회전 · revoke · 응급 교체할 때 따르는 절차입니다.

언제 회전하나

트리거권장 빈도/대응revoke_reason
정기 (preventive)분기 1회 또는 90일rotation
운영자 이직 · 자격 회수즉시admin
Secret 노출 의심 (CI 로그, ticket 첨부 등)즉시 + grace 생략compromise
Grace window 만료 후 정리24h 경과 후 cleanuprotation_grace_expired

revoke_reason 값은 WebhookSigningKey::REVOKE_REASONS 상수로 강제됩니다. 외부 운영 도구에서 값을 임의로 넣지 마세요.

회전 메커니즘

logi 는 rake 태스크로 회전을 수행합니다. CLI 또는 admin UI 가 아니라 Render shell / 로컬 console 에서 실행:

bash
# 1) 신규 키 발급 — 기존 키는 그대로 유효
bin/rails 'webhooks:rotate[<oauth_app_id>]'
# → Issued new signing key: kid=ab12cd34
#     secret=<plaintext>  ← 1회만 출력. 즉시 secret manager 또는 안전한 채널로 전달

이 시점부터:

  • 새 outbox row 의 서명은 가장 최근 unrevoked 키 (WebhookSigningKey.active_for) 로 계산
  • 이미 발송 대기중인 row 는 자기 signature_kid 그대로 유지 — RP 는 그 kid 로 검증해야 함
  • 기존 키는 revoke 하지 않는 한 계속 유효 — RP 가 새 kid 를 풀(GET /api/v1/webhook_signing_keys) 할 시간을 확보

회전 후 RP 가 catch up 했다고 판단되면 구 키를 revoke:

bash
bin/rails 'webhooks:revoke[<oauth_app_id>,<old_kid>]'

webhooks:revoke 는 "마지막 active 키"를 거부합니다 — 회전 전에 revoke 부터 시도하면 즉시 중단:

Refusing to revoke last active key — run webhooks:rotate first.

Grace window 의 의미

DB 스키마 주석은 revoked_at = now + 24h 패턴 (미래 시각으로 revoke 예약) 을 가정합니다. 모델의 usable scope 도 revoked_at IS NULL OR revoked_at > now — 즉 미래 시각의 revoke 는 grace 동안 검증 통과. 현행 webhooks:revoke rake 태스크는 Time.current 로 즉시 revoke 합니다. 24h grace 가 필요하면 회전 후 24h 대기 후 revoke 를 호출하는 운영 패턴으로 운용하세요.

RP 측 대응

RP 는 GET /api/v1/webhook_signing_keys 로 active 키 목록을 정기 fetch 해야 합니다. Auth 는 HTTP Basic + client_id:client_secret.

bash
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  https://api.1pass.dev/api/v1/webhook_signing_keys

응답 형태:

json
{
  "keys": [
    {
      "kid":         "ab12cd34",
      "secret":      "<hex>",
      "algorithm":   "HMAC-SHA256",
      "issued_at":   "2026-05-11T10:00:00Z",
      "expires_at":  null,
      "revoked_at":  null,
      "revoke_reason": null
    }
  ]
}

RP 구현 시 필수 사항:

  • 다수 키 동시 캐시 — 신/구 키를 모두 보유. 단일 키만 들고 있으면 회전 순간 검증 실패
  • kid 기준 lookup — 수신 webhook 의 X-Logi-Signature 헤더에서 kid=<값> 을 파싱하고, 캐시에서 해당 키를 조회 (v1=<hmac> 비교 전 단계)
  • 캐시 갱신 주기 — 최소 5분에 1회. kid miss 가 발생하면 즉시 강제 refetch (1회 한정 — 회전 직후 캐시 stale 케이스)
  • 만료 키 정리revoked_at 이 과거인 키는 즉시 캐시에서 제거

응급 회전 (compromise)

Secret 노출이 의심되면 grace window 없이 즉시 교체합니다. 단일 rake 태스크로 원자적으로 처리:

bash
bin/rails 'webhooks:compromise[<oauth_app_id>,<compromised_kid>]'

이 태스크는 하나의 DB 트랜잭션 안에서 다음을 수행:

  1. 새 키를 먼저 발급 (compromised 키가 유일한 active 키였을 경우를 대비)
  2. compromised 키를 revoked_at = Time.current, revoke_reason = "compromise" 로 즉시 hard-revoke
  3. 아직 발송되지 않은 outbox row (delivered_at IS NULL AND dlq_at IS NULL) 중 signature_kid 가 compromised 인 것을 모두 새 키로 re-sign
  4. webhook_key.compromised 이벤트를 outbox 에 기록 — RP 가 webhook 으로 통지받음

webhook_key.compromised 이벤트 payload:

json
{
  "revoked_kid": "ab12cd34",
  "revoked_at":  "2026-05-11T10:30:00Z",
  "reason":      "compromise",
  "active_kids": ["ef56gh78"]
}

이 이벤트를 받은 RP 는 즉시 캐시를 정리하고 /api/v1/webhook_signing_keys 를 refetch 해야 합니다. 단, 이 통지 이벤트 자체는 새 키로 서명되어 있으므로 RP 가 새 키를 못 받았으면 검증 실패 — out-of-band 채널(Slack/이메일)로 동시에 통지하세요.

Out-of-band 통지

compromise 시 RP 운영자에게 webhook 외 채널로도 알리는 것이 안전합니다. 현재 logi 는 webhook 자체 이외의 자동 통지 채널을 운영하지 않습니다 — 운영자가 RP 의 통합 담당자에게 직접 연락하는 절차를 미리 정의해 두세요.

감사 (audit)

회전 · revoke · compromise 동작은 모두 Rails.logger 에 다음 prefix 로 기록됩니다:

  • [webhooks:rotate] app_id=<id> new_kid=<kid>
  • [webhooks:revoke] app_id=<id> kid=<kid>
  • [webhooks:compromise] app_id=<id> revoked_kid=<kid> new_kid=<kid> resigned=<n> (WARN 레벨)

Render log → 검색 prefix [webhooks: 로 모든 키 lifecycle 이벤트를 한 화면에서 확인 가능. 분기별 회전 시 운영 로그에 함께 첨부하세요.

회전 체크리스트

  • [ ] 신 키 발급 직후 plaintext 가 안전한 채널로 RP 운영자에게 전달됨
  • [ ] RP 가 /api/v1/webhook_signing_keys 풀에서 신 kid 가 보이는 것 확인
  • [ ] 신 키 발급 후 최소 한 번의 실제 webhook 이 신 kid 로 서명되어 RP 가 검증 성공
  • [ ] (정기 회전의 경우) 24h 이상 grace 경과 후 구 키 revoke
  • [ ] (compromise 의 경우) webhook_key.compromised 이벤트가 모든 active RP 에 전달됨 + out-of-band 통지 완료
  • [ ] Render log 에서 [webhooks:*] 라인 확인 후 운영 로그에 첨부

MIT License · Identity가 제품의 신뢰를 만듭니다.