RCA — BCP vault backup pipeline produced zero backups since inception (silent no-op from 2026-06-30)
Incident ID: 2026-08-29-bcp-vault-backup-silent-noop
Date discovered: 2026-08-29 (via prior read-only cross-repo audit, TradeMasterAPI issue #4535)
Severity: SEV-3
Duration: From the original GitHub Actions workflow (2026-05-29, #3049) until at least 2026-10-30, the daily vault snapshot has never produced a backup object in s3://raxx-iac-state-prod/vault-exports/. It reported green (exit 0) from 2026-06-30 (soft-skip added to the GHA workflow) until 2026-09-11 (#4598, hard-fail on develop); before 2026-06-30 it failed red at the GPG step with a cryptic error, and the key is still not confirmed provisioned (see Action items).
Blast radius: vault.raxx.app (self-hosted Infisical) had zero off-host encrypted backups the entire time. 22 sibling repos/services depend on this vault for secrets. No customer-facing surface affected directly, but a total vault loss during this window (host failure, corruption, compromise) would have had no BCP recovery path beyond the separate Lightsail AutoSnapshot (7-day retention, whole-disk, not app-level export).
Author: sre-agent
Timeline of the preflight behaviour (verified against the commit history, UTC)
| Date | Event |
|---|---|
| 2026-05-29 | GHA workflow bcp-vault-snapshot-daily.yml introduced (#3049). GPG_BACKUP_PUBLIC_KEY is never provisioned |
| 2026-05-31 | CF Access header fix for the Infisical REST calls; runs still fail red at the GPG fingerprint-parse step |
| 2026-06-30 | GHA preflight added: missing key prints a ::warning:: and exit 0. The job turns green while still writing nothing |
| 2026-07-05 | Woodpecker port (#4051, issue #4016) carries the exit 0 soft-skip over |
| 2026-08-29 | Gap found via issue #4535; this RCA and the runbook refresh are written on fix/vault-backup-pipeline |
| 2026-09-11 | exit 1 hard-fail lands on develop through #4598 (the same change as this branch's pipeline edit, made independently) |
Summary
The vault-export step of .woodpecker/bcp-vault-snapshot-daily.yaml has a preflight check for GPG_BACKUP_PUBLIC_KEY (required to encrypt the export before it leaves the host). GPG_BACKUP_PUBLIC_KEY was never provisioned at Infisical path /MooseQuest/bcp/ (originally it was expected as a GitHub Actions repo secret). When the preflight found the key empty, it printed a warning and executed exit 0 — a soft skip that reports pipeline success. Every subsequent step (GPG import, export, upload, S3-size verification) was simply never reached, and the notify-on-failure step (Slack/Postmark page) never fired because the pipeline never registered as failed. The result: a backup pipeline that ran daily, reported green from 2026-06-30 onward, and never once wrote a backup — an invisible BCP gap that would only have been discovered at restore time.
Root cause analysis
- Primary cause — success/failure signaling was inverted for the one condition that matters most. A missing encryption key is not a benign, skippable condition for a backup pipeline; it is the definition of backup failure (no artifact can be produced). The preflight treated it as the former (
exit 0) instead of the latter (exit 1), which suppressed the pageable failure path entirely. - Contributing factor — the key was never provisioned in the first place.
GPG_BACKUP_PUBLIC_KEYat/MooseQuest/bcp/was never set when the pipeline was built or ported (issue #4016 was the Woodpecker port; the original workflow is #3049). Because the pipeline didn't hard-fail, this gap was never surfaced as an alert, a failed CI run, or anything else an on-call engineer would see. - Contributing factor — the documented runbook remediation (
docs/ops/runbooks/vault-disaster-recovery.md, "BCP backup failure" section) was written for the pipeline's prior GitHub Actions incarnation (gh secret set,workflow_dispatch) and had not been updated for the Woodpecker/Infisical port (ADR-0135, Wave A batch 2). A responder following the runbook literally would have run commands that don't apply to the current tooling. - No "zero backups produced" alerting existed independent of the pipeline's own (broken) exit code — the only signal of pipeline health was the pipeline's own status, which is exactly the signal that was compromised.
Detection
- What alerted us: a prior read-only cross-repo SRE audit inferred the gap from Infisical path enumeration (no
GPG_BACKUP_PUBLIC_KEYat/MooseQuest/bcp/) and flagged it as TradeMasterAPI issue #4535. There was no automated detection from the pipeline itself at any point. - Time between cause and detection: about three months (2026-05-29 to 2026-08-29), of which the last two months reported green (2026-06-30 onward).
- How to detect faster next time: (1) the preflight now hard-fails and pages instead of silently succeeding (this fix); (2) recommend an independent, pipeline-external check that alerts if no new object has landed in
s3://raxx-iac-state-prod/vault-exports/within the last 25 hours, so a different failure mode in the pipeline itself (e.g. the whole step being skipped by a Woodpecker config error) can't reproduce this same blind spot (see action items).
Resolution
- What was changed (this fix, sre-agent,
fix/vault-backup-pipeline): 1..woodpecker/bcp-vault-snapshot-daily.yamlpreflight step changed fromexit 0(silent skip) toexit 1(hard fail) whenGPG_BACKUP_PUBLIC_KEYis empty, so the existingnotify-on-failurestep (Slack + Postmark, already correctly wired to firewhen: status: failure) actually pages as originally intended. This landed ondevelopthrough #4598 (2026-09-11), not through the PR that carries this RCA; that PR now contains only this RCA and the runbook refresh. 2.docs/ops/runbooks/vault-disaster-recovery.md"BCP backup failure" section updated to match the current Woodpecker/Infisical pipeline (was still describing the retired GitHub Actions/gh secret setprocedure), and now documents thervault setprovisioning path (with the manifest caveat:rvault sethas no--pathflag), the base64 encoding the pipeline expects, a warning thatscripts/ops/secrets/mint_gpg_backup_key.shstill targets a GitHub Actions secret, and the bootstrap-circularity reason the private key must never be stored in Infisical itself. - What was not completed by this agent (see Follow-ups / blocker below): actually generating the GPG backup keypair and provisioning
GPG_BACKUP_PUBLIC_KEYinto/MooseQuest/bcp/, and the corresponding end-to-end verification of a real object landing in S3. This requires an Infisical admin/write identity (rvault:adminin macOS Keychain,~/.config/rvault/admin.env, orRVAULT_ADMIN_CLIENT_ID/RVAULT_ADMIN_CLIENT_SECRET), all of which this agent's sandboxed session was denied read access to by its own permission controls. This is being reported as a hard blocker rather than worked around.
Action items
Status as of 2026-10-30 UTC (from the repo and issue #4535): #4535 is still open and labelled operator-blocked. Nothing on develop shows the key provisioned or a real object verified, so items 1 and 2 are open and past due. Item 3 is only partly covered: bcp-smoke-monthly Check 5 (.woodpecker/bcp-smoke-monthly.yaml) looks for yesterday's vault-exports/ object, but only monthly, not within 25 hours. There is no evidence of items 4 and 5 on develop.
| # | Action | Owner | Due | Issue |
|---|---|---|---|---|
| 1 | Generate a dedicated BCP backup GPG keypair; store the private key offline (operator-controlled, outside Infisical, per the bootstrap-circularity note in the updated runbook); provision the public key as GPG_BACKUP_PUBLIC_KEY at /MooseQuest/bcp/ via rvault set (admin identity required) |
operator | 2026-09-05 | #4535 |
| 2 | After (1), trigger bcp-vault-snapshot-daily manually in Woodpecker and confirm a .json.gpg object > 1KB lands in s3://raxx-iac-state-prod/vault-exports/ with today's date |
operator or sre-agent (once key is provisioned) | 2026-09-05 | #4535 |
| 3 | Add an independent, pipeline-external alert (e.g. a lightweight cron/CloudWatch check) that fires if no new object has appeared under s3://raxx-iac-state-prod/vault-exports/ in the last 25 hours — so a future failure mode that isn't "step exits early" (e.g. the whole job silently not scheduled) is still caught |
sre-agent | 2026-09-12 | — |
| 4 | Schedule a periodic (quarterly) restore drill: decrypt a real .json.gpg backup object with the offline private key and confirm the exported JSON is well-formed and non-empty, to catch corruption/format regressions that a size-only check (> 1024 bytes) would miss |
operator | 2026-09-26 | — |
| 5 | Audit .woodpecker/*.yaml for other VAULT_ALLOW_MISSING=true + preflight patterns that exit 0 on a missing secret in a pageable pipeline; confirm each one's silent-skip behavior is actually intended (e.g. genuinely optional secrets) rather than another instance of this same signaling bug |
sre-agent | 2026-09-19 | — |
References
- Issue: #4535 (prior read-only cross-repo audit; not closed by this fix — closing requires action items 1-2 to complete, which are blocked pending operator vault-admin action)
- Pipeline:
.woodpecker/bcp-vault-snapshot-daily.yaml - Runbook:
docs/ops/runbooks/vault-disaster-recovery.md(updated as part of this fix) - Vault tooling:
vault-kit(bin/rvault,scripts/mint-identity.sh) - Original pipeline issue: #4016