Operational procedure hardening — ADR-030 S-6/Q-3 (docs only)
SummarySprint 2 of the ADR-030 remediation roadmap. Two undocumented operational gaps closed with zero code changes — runbook-only. S-6: GITHUB_TOKEN_ENCRYPTION_KEY rotation runbook (encryption-key-rotation.md) — dual-key unsupported means rotation invalidates all existing encrypted tokens, but worker.ts fallback path (GitHub App Installation Token) keeps the service live. 3-Secret (gateway-secrets + github-worker-secrets + identity-service-secrets — the 2-Secret draft was caught by Critic R1 P1 for the missing identity service, expanded after code-level verification) simultaneous replacement in a single aether-gitops commit + 4-point verification gate (sha256 match / positive / fallback / closed). Q-3: DLQ redrive runbook (dlq-redrive.md) — topology table (submission.events → 2 queues → 2 DLQs), 5-reason table, root-cause-removal pre-gate (reason-based branch), dynamic shovel recommended + rabbitmqadmin fallback, idempotency caveat (github-worker redis TTL 1 h). 4 cross-ref updates + ADR-030 S-6/Q-3 Sprint 240 ✅. Verification: 5 ADR gates + doc-ref-lint PASS.
Goal
- Item 2 on the ADR-030 remediation roadmap — handle the two operational-procedure items (S-6, Q-3) in a single sprint.
- Minimal code change — focus is on runbook quality and verification gates.
- Close the undocumented operational gaps left after Sprint 239's code quick wins.
Context
/startargument: process ADR-030 §Decision roadmap item 2, S-6 + Q-3.- S-6:
GITHUB_TOKEN_ENCRYPTION_KEYis dual-managed across two SealedSecrets (gateway-secrets, github-worker-secrets) with no documented rotation procedure. A mismatch causes decryption failure. - Q-3: DLQ message redrive is manual but undocumented.
oncall-alerts.md:111ended with "DLQ messages are processed manually/in batch" — one line, no procedure.
Work summary (Architect + Postman + Scribe, 3 commits total + Critic R1 corrections)
S-6 — New runbook encryption-key-rotation.md (Architect) + Critic R1 corrections
- Source files read directly:
services/gateway/src/auth/oauth/token-crypto.util.ts(encryption),services/github-worker/src/worker.ts:384-398(fallback path),infra/sealed-secrets/sealed-secrets-template.yaml:58,164(two SealedSecret locations — initial read),docs/runbook/key-rotation.md(kubeseal conventions). - §0 Background & impact: AES-256-GCM symmetric key; ciphertext format
iv:ciphertext:tag. Dual-key unsupported — rotating the key immediately invalidates all existing encrypted tokens. However, theworker.ts:384-398fallback (GitHub App Installation Token) absorbs decryption failures and keeps the service live. Users re-encrypt on their next GitHub re-link. Bulk migration is not needed under the current design. Rotation triggers (scheduled / suspected leak) distinguished. - §1 Pre-flight checklist: kubeseal · kubectl · aether-gitops clone up to date (reuses
key-rotation.mdpattern). - §2 New key generation:
openssl rand -hex 32+ 64-char hex format validation. - §3 Initial 2-Secret → Critic R1 corrected to 3-Secret simultaneous replacement: Critic flagged missing identity service (P1). Oracle confirmed via code:
services/identity/src/user/token-encryption.service.ts:28-34— throws on boot ifGITHUB_TOKEN_ENCRYPTION_KEYis unset (live identity running = key exists in live secret);user.service.ts:136uses encryption. Updated from 2-secret to 3-secret (gateway-secrets + github-worker-secrets + identity-service-secrets): §0 management table gained identity row, §3 gained identity-service-secrets regeneration step (9 keys), §4 sha256 comparison expanded to 3-secret, §5 rollback updated to 3-secret simultaneous restore.infra/sealed-secrets/sealed-secrets-template.yamlidentity-service-secrets section gainedGITHUB_TOKEN_ENCRYPTION_KEYentry (template drift closed). Full key-list inclusion warning (same askey-rotation.md §3). - §4 Verification gate (4 points): ① 3-Secret sha256 match (without exposing plaintext) ② New GitHub OAuth link positive test ③ Existing-token
GITHUB_APP_FALLBACKlog = expected signal ④ Error rate and DLQ unchanged (closed-circuit proof). - §5 Rollback: single-commit revert of all 3 Secrets to old key + ArgoCD sync + 3-service rollout restart.
- §6 Post-rotation cleanup: delete plaintext key files, record rotation in log table.
- §7 Related docs: key-rotation.md, github-token-relink.md, sealed-secrets-template.yaml, ADR-030.
- Critic R1 P2: Added explicit
kubectl rollout restart deployment/{gateway,github-worker,identity-service} -n algosu+ rollout status for 3 services after §3 ArgoCD sync — SealedSecret updates do not guarantee automatic pod restarts; this is now stated explicitly in the runbook.
Q-3 — New runbook dlq-redrive.md (Postman) + Critic R1 correction
- Source files read directly:
services/submission/src/saga/mq-publisher.service.ts:72-105(DLQ topology declaration),services/github-worker/src/worker.ts:174-180,200,269(DLQ NACK + reason labels),services/ai-analysis/src/worker.py:149-156,316,370,384(DLQ NACK + reason),docs/runbook/oncall-alerts.md:103-111(DLQReceived alert). - §0 Background & topology:
submission.events(topic) →submission.github_push/submission.ai_analysis; NACK(requeue=false) →submission.events.dlx→submission.github_push.dlq(routinggithub.push.dead) /submission.ai_analysis.dlq(routingai.analysis.dead). 5-reason table:parse_error/process_failure(both workers),circuit_breaker_exhausted/rate_limit_exhausted(ai-analysis only),token_invalid(github-worker only). - §1 Pre-redrive mandatory gate: root cause must be resolved first — reason-based branches:
parse_erroris a publisher schema defect, so redriving immediately re-fails (meaningless);process_failure— confirm downstream recovery;circuit_breaker_exhausted/rate_limit_exhausted— confirm CB CLOSED and rate normalized. - §2 DLQ inspection: kubectl exec + rabbitmqctl for queue depth/message peek; management plugin availability verified as a pre-check step.
- §3 Redrive procedure: recommended = dynamic shovel (DLQ → original exchange + routing key, no message loss, delete shovel when done); alternative = rabbitmqadmin manual loop (for small counts).
- §3 Method B Critic R1 P2 fix: The initial Method B used
ackmode=ack_requeue_falseto consume the message while extracting payload viagrep -oP— ①--format=jsonwas absent making parsing unreliable ② escaped quotes in JSON payload caused truncation → truncated payload published + original permanently lost. Reconstructed as peek → validate → publish → consume:ack_requeue_true --format=json | jq -r '.[0].payload'for safe extraction →jq -e '.submissionId'for JSON validity gate → only after confirmed publish success doesack_requeue_falseremove the original. Method B data-loss warning strengthened. - §4 Idempotency & duplicate risk: github-worker redis key
ghw:processed:{submissionId}TTL 1 hour — redrive within 1 hour is auto-skipped (safe), after 1 hour a duplicate GitHub push occurs (assess impact). ai-analysis has no idempotency guard; result overwrite is accepted. - §5 Verification: DLQ depth 0 + worker processing logs +
dlq_messages_totalno new increment + submission status transition confirmed. - §6 Automation decision criteria: if monthly occurrence exceeds N times, evaluate automation — makes the ADR-030 "confirm frequency first" decision operationally visible.
- §7 Related docs: oncall-alerts.md, mq-publisher.service.ts, github-token-relink.md, ADR-030.
Index & cross-reference updates (Scribe)
docs/runbook/README.md: GitHub/auth 4→5 (added encryption-key-rotation), observability/monitoring 2→3 (added dlq-redrive).docs/README.md: runbook count 21→23; both section table entries updated.docs/runbook/oncall-alerts.md: DLQReceived response "수동/배치 재처리" → explicit reference todlq-redrive.md(root-cause removal first, noted).docs/runbook/key-rotation.md: comment cross-ref toencryption-key-rotation.mdon theGITHUB_TOKEN_ENCRYPTION_KEYline.docs/adr/ADR-030-security-improvement-backlog.md(+EN): S-6/Q-3 rows →Sprint 240 ✅; roadmap table Sprint 240 row → ✅.docs/adr/README.md: sprint ADR count 177→178; range Sprint 62239 → 62240.
Key decisions
- Dual-key rotation = existing-token invalidation, but service stays live: the
worker.tsfallback path (GitHub App Installation Token) absorbs decryption failures automatically. The runbook explicitly marks this as "expected behavior" — "after rotation,GITHUB_APP_FALLBACKlogs are a normal signal, not an alert". Without this framing, an operator would mistake fallback logs for a production incident. - Distinguishing single-commit replacement from Sprint 236's 2-commit ordering: Sprint 236 added a new secret (pod would fail to mount if absent) → 2-commit add-then-switch. Sprint 240 replaces an existing secret → single commit to eliminate the mismatch window. The same "commit carefully" principle leads to opposite strategies depending on whether the resource is being added or replaced.
- DLQ redrive requires root-cause removal as a pre-condition:
parse_erroris a publisher schema defect — redriving re-fails immediately → redrive is pointless. The runbook's §1 reason-based gate blocks the "just redrive it" anti-pattern. - Dynamic shovel recommended; rabbitmqadmin is the debug fallback: shovel achieves atomic move without message loss (including large volumes). Manual rabbitmqadmin loop is for small-scale debugging. The automation threshold in §6 operationalizes ADR-030's "confirm frequency first" by giving it a concrete measurable condition.
- Docs-only sprint still goes through Critic cross-review: runbook command accuracy (exchange names, routing keys, secret key names) carries the same risk of production failure as code bugs.
Verification
- 5 ADR gates (
node scripts/check-adr-index.mjsetc.) PASS: index 178 / EN 188/188 / links 0 / doc-refs / conversion. node scripts/check-doc-refs.mjs(doc-ref-lint): all cross-ref links in the two new runbooks valid, 0 errors.- Manual command consistency check: exchange/queue/routing key names verified against
mq-publisher.service.ts:72-105declaration; SealedSecret key names verified againstsealed-secrets-template.yaml:58,164— match confirmed. - Critic cross-review (Codex,
--base 924c650): R1 findings → corrections applied- [P1] S-6 identity service missing from secret rotation scope: Oracle confirmed via code (
token-encryption.service.ts:28-34boot throw,user.service.ts:136encryption) → 2-secret expanded to 3-secret throughout runbook +sealed-secrets-template.yamlidentity-service-secrets section gainedGITHUB_TOKEN_ENCRYPTION_KEY(template drift closed). - [P2] S-6 no rollout restart after SealedSecret update: added
kubectl rollout restart deployment/{gateway,github-worker,identity-service}+ rollout status for 3 services; explicit note that SealedSecret updates do not guarantee automatic pod restarts. - [P2] Q-3 Method B message-loss risk: reconstructed as peek → validate → publish → consume;
ack_requeue_true --format=json | jqfor safe extraction;ack_requeue_falseonly after confirmed publish success. Data-loss warning strengthened.
- [P1] S-6 identity service missing from secret rotation scope: Oracle confirmed via code (
- auto-critic R2: with an invalid message stuck at the queue head, Method B's
continuere-peeks the same message forever → fixed withbreak+ manual-removal guidance (df14b84). auto-critic R3 CLEAN. - Merge-gate Critic R1–R5 (Codex, full-branch
--base 924c650): R1 — [H-1] a cross-ref comment inserted inside the multi-line bash command inkey-rotation.mdsevered the line continuation, producing a SealedSecret missing every subsequent key (a regression introduced by this sprint) + [M-1] ADR-030 S-6 row still saying 2-location/2-key + [L-1–L-3] §1 identity omission, §6 cleanup omission, line ref:164→:167→ all 5 fixed. R2 — gate 2grep -vmisjudgment (always passes) + jq install note targeting the pod instead of the local shell → fixed. R3 — gate 2 bidirectional misjudgment (false failure from concurrent users' fallback / false success on kubectl failure) → fixed with traceId scoping + 2 pre-gates. R4 — pre-gates echo but flow continues, still reaching a false ✅ → fixed with a single exclusive if/elif/else verdict. R5 CLEAN ✅. - Changed files:
docs/runbook/encryption-key-rotation.md,docs/runbook/dlq-redrive.md,docs/runbook/key-rotation.md,infra/sealed-secrets/sealed-secrets-template.yaml,docs/adr/ADR-030-security-improvement-backlog.md(+EN),docs/adr/sprints/sprint-240.md,docs/adr-en/sprints/sprint-240.md.
Carry-overs
- Sprint 241 confirmed: Q-1 (BE) study.service domain split, Q-2 saga-orchestrator helper extraction — ADR-030 §Decision roadmap.
- ThrottlerGuard for /api/events (backlog, Sprint 239 carry-over).
- Existing carry-overs: harness checkup in separate slot · GA4 console 3 items · live SEO · harness cron · webhook regenerate · cumulative UAT · blog backlog (CS quiz / deleted features / zstd).