HOMELAB-2281: unsuspend etcd snapshot mirror (object-lock destination) #684
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!684
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2281-etcd-mirror"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rollout step runbook §4.1 #4, third and final consumer on HOMELAB-2281 (order: OpenBao ✅ → OPNsense (blocked on the router 500, untouched) → etcd).
What changes
One functional line in
core/manifests/etcd-backup/mirror-cronjob.yaml:Non-comment diff lines, filtered: exactly the two lines above.
1 file changed, 1 insertion(+), 1 deletion(-). No other manifest, script, fixture, alert or CRD is touched. Nothing is applied to the cluster from this branch.Deliberately unchanged (the design invariants this flip must not disturb), verified mechanically after the edit — 29/29 checks:
concurrencyPolicyForbid(one-writer serialization)restartPolicyNever(a restart would reuse the emptyDir status marker and could report a stale success)activeDeadlineSeconds1800;startingDeadlineSeconds180010 3 * * *— 40 min after the primary (02:30, deadline 1200s)s3 syncexcludes--exclude "locks/*"and--exclude "*-tmp-*"(pull and push)--deletes3 ls …/repo/locks/must be empty; non-empty →ERROR: lock/other objects exist …+exit 1--no-lock(3/3), and the newest snapshot must equaldate -u +%F(today-UTC freshness gate)EtcdMirrorStale/EtcdMirrorNeverSucceededunless on() (kube_cronjob_spec_suspend{namespace="etcd-backup",cronjob="etcd-snapshot-mirror"} == 1)ROT_AWS_ACCESS_KEY_ID/ROT_AWS_SECRET_ACCESS_KEYfrometcd-snapshot-b2-locked; no secret ref isoptionalGating evidence (already recorded on HOMELAB-2281)
etcd-snapshotkey, including the lock proof — test 4, the pruner key (which hasdeleteFiles, nobypassGovernance) deleting a freshly uploadedetcd-snapshot/_locktest/…object:An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied, and the version was still readable by itsversionIdafterwards ({"Len": 40}). Capability denials readnot entitled; this one readsAccess Denied— the store refusing, not the key's permissions. Tests 3 (retention-shorten / legal-hold-off →not entitled), 5 (403 Forbiddencross-prefix read), 5b (cross-prefix write refused, 0 such objects) also PASS.Secret etcd-backup/etcd-snapshot-b2-lockedisReady=True reason=SecretSynced(ClusterSecretStoreopenbao, remoteb2-root-of-trust/etcd-writer), with exactly the two keys[ROT_AWS_ACCESS_KEY_ID, ROT_AWS_SECRET_ACCESS_KEY](b64 36/44).verified_locked s3://reynoza-root-of-trust/openbao-raft/openbao-2026-10-30.snap bytes=221227 sha256=9bb4f0dc…76cdc (append-only, no prune), the locked object was downloaded and hashed independently to the same sha256, and the oldreynoza-velerocopy is unchanged.Expected live effect (first unsuspended run: 2026-10-30T03:10Z, ~1 h after this PR)
mirrorpullsreynoza-velero/etcd-snapshot/into the emptyDir scratch dir (excludinglocks/*,*-tmp-*), then pushes it toreynoza-root-of-trust/etcd-snapshot/repo/— the first object copy into the locked bucket for etcd.…/repo/locks/must list nothing. One lock object there is permanent (30-day governance retention, no cluster key hasbypassGovernance), so this is a hard failure, not a warning.verifywaits for the mirror's status marker, then opens the locked repo read-only (restic --no-lock) and requires the newest snapshot to be today's UTC date.1in the status marker, so the verifier fails instead of waiting, and the Job fails as a unit.etcd-snapshotJob is untouched and keeps its own meaning — separate CronJob, no locked-bucket credential in it, its own read-back proof remains the only thing that decides whether etcd is backed up today.Alerts arm with this flip. The
suspend: falseguard stops suppressingEtcdMirrorStaleandEtcdMirrorNeverSucceeded(bothwarning, both already loaded in Prometheus withhealth=ok).EtcdMirrorNeverSucceededfires if no success timestamp exists 26 h after the flip;EtcdMirrorStalefires when the last success is older than 26 h. A red mirror means "the copy nobody can delete is missing", not "the backup failed".Non-goals
etcd-snapshotkeeps writingreynoza-velero/etcd-snapshot/exactly as before; this PR adds a copy, not a migration.reynoza-velerocopies. Runbook §4.1 step 5 requires a full week of green nightly runs across all three consumers, and retiring root-of-trust data is its own decision in its own ticket.Local validation on this branch
ConfigMap,CronJob);spec.suspendreads the booleanFalse.--deletecheck ignores comment text, and the locks assertion is anchored to the code path, not a grep hit).scripts/root-of-trust/tests/test-etcd-mirror-fixture.sh: 12/12 fixtures pass, 0 failures.One finding for the reviewer (not fixed here, because the brief scoped this PR to the single flag line)
scripts/root-of-trust/tests/test-etcd-mirror-fixture.shline 211 asserts the parked state with a whole-filegrep -q 'suspend: true' "$MANIFEST". After this flip that grep still succeeds — it matches the design comment at line 53 (# ENABLING IT: `suspend: true` is the flag …), not the spec field. So the §6.1 gate "mirror ships suspended" is now a false positive: it passes for an enabled mirror, and would also pass if someone re-suspended. A test that cannot fail is not a test (the same standard runbook §4.1 step 3 applies to the live tests).Suggested fix, one line in the fixture, in this PR or as a fix-worker commit on this head:
Anchoring with
^ suspend:also makes the assertion immune to the comment in either direction.I did not review or merge this PR. Ticket stays In Progress; it closes on a green first mirror Job (
mirror_ok … locks=0,locked_copy_readback newest_snapshot=2026-10-30,locked_copy_verified), not on this merge.Review — HOMELAB-2281 unsuspend etcd snapshot mirror (round 1)
Reviewed head
f6657d6bfrom primary sources only: the diff, the full manifest at head (blob4f55e79, matching the diff's post-image),core/manifests/monitoring/rules/backup-alerts.yaml, the fixture script, and CI.(1) Diff is exactly the one-line flip
pulls/684.diffis one hunk, one file,1 insertion(+), 1 deletion(-):core/manifests/etcd-backup/mirror-cronjob.yaml:216suspend: true→suspend: false. No comment churn, no ad-hoc edits riding along. Parses to the booleanFalse(not the string).(2) Design invariants still intact in the head file
concurrencyPolicy: Forbid(L217),startingDeadlineSeconds: 1800,activeDeadlineSeconds: 1800(L230),backoffLimit: 1,restartPolicy: Never(L241) with the documented reason (a restart would reuse the emptyDir status marker and could report a stale success).s3 synccalls carry both excludes (--exclude "locks/*" --exclude "*-tmp-*"), pull L128-129 and push L135-136 — the deliberate second filter is still there.--deleteappears nowhere as an argument (only in the "Never--delete" comment). Append-only push preserved.locks=$(dst s3 ls "${DST_REPO%/}/locks/" || true); non-empty →ERROR: lock/other objects exist …+exit 1(L140-146).--no-lock(L175, 176, 191) and the freshness gate is[ "$newest" != "$(date -u +%F)" ]→exit 1(L193-195).optional:in the file): srcetcd-snapshot-b2, dst and verifyetcd-snapshot-b2-lockedwith keysROT_AWS_ACCESS_KEY_ID/ROT_AWS_SECRET_ACCESS_KEY. The preflight'sROT_*names match the env names, so the credentials-file heredoc cannot silently get an empty credential.workemptyDir at/workandSTATUS_FILE=/work/mirror-status, so the flip does not expose a status-file path mismatch.(3) Primary job untouched
The diff touches no other path, so
core/manifests/etcd-backup/cronjob.yaml(blob48f6b04, last changed 2026-10-28) is byte-identical. The mirror is additive; the primary's read-back proof still decides whether etcd is backed up today.(4) CI green on head
Run #2371 (
shellcheck,yaml-lint,terraform-validate) and run #2370 (alert-receiver-test,cockpit-e2e,cockpit-test,dispatcher-test,helm-validate,receiver-test,record-sql-test) — all success onf6657d6b.(5) Stated live effect is honest
schedule: "10 3 * * *"→ the claimed first run at 2026-10-30T03:10Z is right for a UTC controller clock, and the manifest already documents the 40-min gap after the 02:30 primary.EtcdMirrorStaleandEtcdMirrorNeverSucceededcarryunless on() (kube_cronjob_spec_suspend{…} == 1), which stops suppressing the momentsuspendreads0.NeverSucceeded(absent(...),for: 26h) fires ~26 h after the flip if no success exists — matching the body.EtcdMirrorStaleneeds an existing success timestamp, which the body states correctly ("when the last success is older than 26 h").trapwrites1to the status file on any non-zero exit, and verify fails on!= 0instead of waiting outMIRROR_WAIT_SECONDS.Findings
Blocker: none open.
Concern (pre-existing, correctly excluded from this diff) — the §6.1 fixture gate is now inert. Confirmed independently, not taken on trust:
scripts/root-of-trust/tests/test-etcd-mirror-fixture.sh:211does a whole-filegrep -q 'suspend: true', which matches the design comment atmirror-cronjob.yaml:53(# ENABLING IT: \suspend: true` is the flag …). The gate therefore passes for an enabled mirror **and** would still pass if someone re-suspended it. The diff touches no fixture, so this is neither caused nor worsened by this PR — but it means green CI is not evidence for the parked/enabled state in either direction. The suggested anchored assertion (grep -qE '^ suspend: false$'`) is the right fix and makes the check immune to the comment both ways. Recommendation: fold that one-liner in on this head, or file it and link it from HOMELAB-2281 — I found no existing Plane ticket for it, so as it stands the follow-up has no owner. Not a merge blocker: the flip is a one-line flag change, and the append-only/lock/no-delete invariants that carry the actual risk are asserted elsewhere and unchanged.Note (no action):
NeverSucceeded's 26 hfor:starts when the series goesabsentat the flip, so the earliest page is ~2026-10-31T05:10Z; the first scheduled run at 03:10Z has a healthy margin inside it.Verdict: blockers: none open. No edits or verdict from me beyond this COMMENT review — I did not touch the branch.