테마
Incident Response 플레이북
⚠️ 운영자 전용 — oncall responder 용. 첫 RP 통합자가 읽을 페이지 아닙니다.
Incident 분류
| sev | 정의 | 대응 시간 |
|---|---|---|
| sev1 | api.1pass.dev/up 5xx/404, 모든 OAuth 토큰 발급 실패, JWKS 응답 불능 — 모든 RP login 마비 | 즉시 |
| sev2 | 일부 endpoint 5xx, latency p95 > 2s 지속, webhook delivery 일괄 실패 | 1시간 내 |
| sev3 | DLQ 적재 (개별 RP), 머지 race condition, 단일 사용자 incident | 24시간 내 |
First-line Triage (모든 sev 공통)
bash
# 1. health
curl -i https://api.1pass.dev/up
# 2. Render service status
curl -s "https://api.render.com/v1/services/<LOGI_WEB_SERVICE_ID>" \
-H "Authorization: Bearer $RENDER_API_KEY" | jq '.serviceDetails.suspended, .status'
# 3. 최근 deploy 5개
curl -s "https://api.render.com/v1/services/<LOGI_WEB_SERVICE_ID>/deploys?limit=5" \
-H "Authorization: Bearer $RENDER_API_KEY" | jq '.[].deploy | {id, status, commit: .commit.id, createdAt}'회귀 의심 시: 직전 green deploy 로 즉시 rollback 후 원인 분석. 배포 Runbook §롤백 참조.
Webhook 실패 / DLQ
DLQ 는 별도 테이블이 아니라 webhook_outbox_entries.dlq_at 상태 입니다 (app/jobs/logi/webhooks/delivery_job.rb 참조). 5xx/408/429 는 retry, 그 외 4xx 는 즉시 DLQ.
모니터링
DB 직접 (read-only) — Render MCP query_render_postgres 사용:
sql
SELECT oauth_application_id, COUNT(*) AS dead_count
FROM webhook_outbox_entries
WHERE dlq_at IS NOT NULL AND delivered_at IS NULL
GROUP BY oauth_application_id
ORDER BY dead_count DESC;또는 admin API: GET /api/v1/admin/webhook_outbox?status=dead (admin 토큰 필요).
재처리
단건 retry (admin UI 권장 — step-up 인증 필요): POST /api/v1/admin/webhook_outbox/:id/retry body: { "action_request_nonce": "..." } → dlq_at, next_retry_at, attempts 초기화 + enqueued_at = now. 다음 dispatcher pass 가 즉시 픽업.
일괄 retry rake task 는 현재 없음. console one-liner:
ruby
# bin/rails runner -e production '...'
WebhookOutboxEntry.where(oauth_application_id: APP_ID).dead.find_each do |e|
e.update!(dlq_at: nil, next_retry_at: nil, attempts: 0, last_error: nil, enqueued_at: Time.current)
endTODO: confirm with ops — 대량 재처리는 admin UI 만으로는 비효율.
webhooks:replay_dlq[oauth_app_id]rake 추가 검토.
흔한 원인
- RP webhook endpoint 5xx (RP 측 incident — RP oncall 에게 escalate)
- RP signature 검증 실패 —
kid가 RP 캐시에 없음. webhook 키 회전 의 grace period 확인 - idempotency 충돌 — 동일
idempotency_key재시도 시 RP 가 409 반환. 정상 동작이므로 DLQ 가 아닌delivered_at으로 마무리되어야 함
머지 Race / 데이터 정합성
identity_links 테이블이 user merge 의 source of truth. EB 같은 RP 는 LogiIdentityLink row 로 link 추적.
canonical resolution cache 강제 무효화
머지된 사용자가 자기 데이터 못 보는 경우 — Rails console:
ruby
# 전체 무효화 (강한 옵션)
Rails.cache.delete_matched("user:canonical*")
# 특정 user 만 (merge_service 와 동일 패턴)
Rails.cache.delete_matched("user:canonical*:#{user.id}*")(app/services/logi/identity/merge_service.rb:245 와 동일 키 패턴.)
ENFORCE_CANONICAL_RESOLUTION flip 주의
⚠️ prod 에서 이 env 켜기 전, 모든 RP 가 LogiIdentityLink row 를 받았는지 확인 (webhooks:backfill_existing_links_to_rp[app_id] 선실행). 미러링 안 된 상태로 flip 하면 머지된 사용자가 RP 측에서 lookup 실패.
backfill rake:
bash
ssh -o StrictHostKeyChecking=no <LOGI_WEB_SERVICE_ID>@<RENDER_SSH_HOST> \
"cd /opt/render/project/src/server && \
/opt/render/project/.gems/bin/bundle exec rails 'webhooks:backfill_existing_links_to_rp[<APP_ID>]' RAILS_ENV=production"idempotent — 이미 발송된 row 는 skip.
JWKS / 키 회전 Incident
kid mismatch 401
RP 가 갑자기 kid 못 찾으면:
- JWKS endpoint 정상 응답 확인:
curl -s https://api.1pass.dev/.well-known/jwks.json | jq '.keys[].kid' - 현재 active
kid가 응답에 포함되어 있는지 - RP 측 JWKS 캐시 TTL — RP 에게 강제 refresh 요청
Webhook signing key 노출 의심
즉시 webhooks:compromise[app_id,kid] rake 실행. 절차는 webhook 키 회전 §응급(compromise) 참조 — atomic transaction 으로 revoke + 신규 key 발급 + 미배달 outbox 재서명 + webhook_key.compromised 이벤트 발송까지 자동.
에스컬레이션
| 단계 | 대상 |
|---|---|
| 1차 | oncall engineer (내부 연락처 별도 문서) |
| 2차 | project owner (내부 연락처 별도 문서) |
| 외부 | Status page 업데이트 (sev1 only) |
sev1 발생 시 5분 이내 1차 → 미응답 시 15분 후 2차. 외부 공지는 RP 통합사에 직접 영향이 갈 때만 (예: api.1pass.dev 30분 이상 다운).
TODO: confirm with ops — Status page URL/도구 미정. PagerDuty/Statuspage.io 도입 여부 결정 필요.