테마
Webhook 서명 키 회전
운영자 전용 페이지
이 페이지는 logi 운영자 (key rotation rake 태스크를 직접 실행하는 인프라/보안 담당) 를 위한 것입니다.
RP 통합 개발자는 다음 4가지만 알면 충분합니다:
- logi 가 webhook signing key 를 회전합니다 (정기 또는 노출 시 즉시).
- RP 는
GET /api/v1/webhook_signing_keys로 active 키 목록을 주기적으로 풀합니다. - 다수 키를 동시에 캐시하고, 수신 webhook 의
kid로 lookup 하세요. kidmiss 시 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 경과 후 cleanup | rotation_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회.
kidmiss 가 발생하면 즉시 강제 refetch (1회 한정 — 회전 직후 캐시 stale 케이스) - 만료 키 정리 —
revoked_at이 과거인 키는 즉시 캐시에서 제거
응급 회전 (compromise)
Secret 노출이 의심되면 grace window 없이 즉시 교체합니다. 단일 rake 태스크로 원자적으로 처리:
bash
bin/rails 'webhooks:compromise[<oauth_app_id>,<compromised_kid>]'이 태스크는 하나의 DB 트랜잭션 안에서 다음을 수행:
- 새 키를 먼저 발급 (compromised 키가 유일한 active 키였을 경우를 대비)
- compromised 키를
revoked_at = Time.current,revoke_reason = "compromise"로 즉시 hard-revoke - 아직 발송되지 않은 outbox row (
delivered_at IS NULL AND dlq_at IS NULL) 중signature_kid가 compromised 인 것을 모두 새 키로 re-sign 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:*]라인 확인 후 운영 로그에 첨부