Skip to content

Incident Response 플레이북

⚠️ 운영자 전용 — oncall responder 용. 첫 RP 통합자가 읽을 페이지 아닙니다.

Incident 분류

sev정의대응 시간
sev1api.1pass.dev/up 5xx/404, 모든 OAuth 토큰 발급 실패, JWKS 응답 불능 — 모든 RP login 마비즉시
sev2일부 endpoint 5xx, latency p95 > 2s 지속, webhook delivery 일괄 실패1시간 내
sev3DLQ 적재 (개별 RP), 머지 race condition, 단일 사용자 incident24시간 내

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)
end

TODO: 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 못 찾으면:

  1. JWKS endpoint 정상 응답 확인: curl -s https://api.1pass.dev/.well-known/jwks.json | jq '.keys[].kid'
  2. 현재 active kid 가 응답에 포함되어 있는지
  3. 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 도입 여부 결정 필요.

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