Files
felhom.eu/REPORT.md
T

7.6 KiB
Raw Blame History

REPORT — R-201 night run: the key came back; the verdict did not

2026-08-04, 21:3022:40, unattended. demo-hp was deliberately rebuilt. No code, no version bump. demo-felhom untouched. Full record: documentation/audits/DRILL-r201-night-run-2026-08-04.md.


1. THE VERDICT — not reached

sentinel sha256, pre-wipe 643166269103a25cf41d34a26b75fd6ebba0a837bbcd7e0d2b5aba7106bcbe7c
sentinel sha256, post-restore not obtained — the restore was never reachable

Not a FAIL. Nothing came back wrong and no fresh history was started; the box never got as far as running a backup. What it is instead: the first proof that the key survives and returns, plus the measured reason a customer still cannot use it.

After a real rebuild — controller data volume destroyed, sentinel deleted from disk — the customer's recovery code produced 8a9e33aa4da6769c5aea1831f87759e10930e2ec1dea0062576484e0598d080a, byte-identical to the pre-wipe on-disk key and to the hub's independent record. That has never been shown before.

2. Snapshot count at step 9 — not obtained

The off-site run was never permitted to start (§4). And a count would have been a poor discriminator anyway: restic's same-day forget --keep-daily 7 --group-by host,tags keeps one snapshot per tag per day, so a successful reopen would have shown 3, not 4. The real observable is whether the pre-wipe snapshot e6132ae5 survives with the sentinel in it — recorded as the resume step.

3. §5's five conditions, recorded before the wipe

  1. sentinel listed by name — snapshot e6132ae5 (19:36:26), -rw-r--r-- 1000 1000 181 …/DRILL-SENTINEL.txt.
  2. rollback archive verifiedvzdump-lxc-9201-2026_08_04-21_44_24.tar.zst, 1 606 765 083 B, full zstd stream read OK (4 867 573 760 B), sentinel confirmed inside it.
  3. §3's option — §5 below.
  4. demo-felhom health=ok, escrow_state=escrowed, untouched throughout.
  5. space — nvme-1tb 883 GB free, root 20 GB, local-lvm 34.32 %.

One precondition had drifted and was repaired, not worked around. The staged snapshot no longer held the sentinel: the afternoon's mandatory data path missing experiment produced a later same-day calibre-web snapshot and forget had pruned the good one. The fixture on disk was correct, so one backup re-established it and it was re-verified by listing. Lesson: a good snapshot is not durable against a later bad run on the same day.

4. Every step's observable

step observable
6 fresh data dir stamped 20:00:2x; new encryption.key (32 B); claimed = None; offbox = null
7 identity_blob 572 B, restic_pw_sha256 8a9e33aa4da6…, updated_at still 11:11:37 — UNCHANGED across the wipe. Nothing re-escrowed itself
8 recovered 8a9e33aa4da6… — matches pre-wipe on-disk and the hub's record. Exit 1 is correct and designed: nothing local to compare against, the rebuilt-box shape
9a [INSTALLED] … reads back identical — the "installed" branch's first real run
9b after the Re-issue and apply, the on-disk key is still 8a9e33aa4da6…WriteOffboxSecrets kept it
9c blocked — see below
1011 not run

The wipe was faithful to the incident, deliberately. The 2026-08-03 rebuild R-193 is filed against was not a guest reprovision — the journal shows guest 9201 running continuously through that window with no pct destroy, no pct restore and no --selftest=provision. What changed was the controller and its data volume. A guest destroy plus an unrehearsed provisioning chain, improvised unattended, is what §8.10 exists to prevent.

5. §3 — the recovery code

Option B as already in place, improved: no new copy was created, so nothing needed shredding. The operator had placed R_DEMO-HP in their own ~/.config/credentials (0600) two sessions ago for this purpose. It was read from there and piped to stdin for the two invocations that needed it — never an argument, never exported, never written to a second file, never logged. Destroying the operator's own store would have destroyed their record; because no additional copy existed, there is nothing left to prove gone.

Verified anyway, with the planted-copy positive control: 0 hits in the agent journal, 0 in the controller log, 0 files under /tmp, /var/tmp, /var/lib/felhom-agent, /root, 0 leftover felhom-idesc-* staging dirs — and the same sweep found a planted copy (1), then 0 after shredding it. The instrument is shown sensitive rather than assumed to be.

6. Part 2 — did not run

Its gate is "the drill PASSED". It did not. A second wipe would have destroyed the state that makes the first drill finishable in five minutes. R-198's retention therefore remains unit-proven only — nothing has yet superseded a key in production.

7. Teardown — three layers

layer state
the guest nothing torn down — the wipe is the evidence; all six app containers serving; the recovered key on disk; scratch band empty
the host vzdump snapshot LVs released cleanly; one new 1.6 GB archive on nvme-1tb (883 GB free); nothing deleted
the hub no new customer recordsdemo-hp is the same row throughout, because the rebuild was a controller-data wipe and not a re-enrolment. One Re-issue staged a one-time secret (consumed 20:16) and set stale_at. The two superseded escrow rows are unchanged — no ceremony ran

Nothing deleted on the storage endpoint, including the ~1.2 GB of orphaned ciphertext — ruled, still owed, deliberately not ridden along with a drill.

8. The capability-map rows

A new row records the key as PROVEN-LIVE after a real rebuild, and states plainly what it does not claim: no file has been restored, and the four links of R-204 stand between the recovered key and a usable repository. The R-199 back-pointer added earlier today stands.

9. New findings

  • R-204 (NEW) — the recovered key cannot be used: a rebuilt box cannot configure its tier (R-193); the Re-issue that fixes that marks the escrow stale though the key never changed (R-196, measured at 20:15:49 with restic_pw_sha256 unchanged); a stale escrow gates every run; and the only way to clear it is a ceremony that destroys the recovered key. Plus a fourth link in no design document: a rebuilt box is unclaimed, so the claim gate intercepts every controller endpoint.
  • R-196 re-scoped — no longer a documentation nit; it is on the critical path for recovery.
  • R-201 — the wipe happened, the key came back, ~5 minutes from a verdict with a person present.
  • R-198 — still unit-proven only; Part 2 gated out.
  • R-202 open; the ciphertext deletion still owed.

10. CI

Docs push only. Run number and task id in the session summary. --no-verify not used.

11. Observations — noticed, NOT acted on

  1. The claim gate is an undocumented first step of every recovery. Before a customer can do anything on a rebuilt box — including recovering their backups — they must re-claim it.
  2. --print-reset-code output needs parsing care: the captured value was 73 characters, i.e. more than the code itself. Not chased; the claim was abandoned when the session stopped.
  3. A same-day re-run replaces the day's snapshot. Worth knowing before designing any drill that depends on a specific snapshot surviving.
  4. The hub's ClusterIP is not reachable from DooPlex's host network — the operator UI needs a kubectl port-forward. The curl -u :$HUB_PW recipe in memory omits that.