v0.88.0: controller-driven escrow ceremony — --output=json machine mode (escrowCeremony extraction, text mode byte-identical), the ONE fixed argv (escrow.CeremonyArgs, shared by exec+manifest+FELHOM_ESCROW sudoers, pin-tested), localapi ceremony job (single-flight, 60s) + one-shot in-memory R claim (10min TTL, unclaimed_void) + preflight; escrow-ceremony capability (Critical, pbs_dr-gated)

This commit is contained in:
2026-07-13 19:01:11 +02:00
parent adf7882f7d
commit 1c3a3ef9ad
11 changed files with 1196 additions and 52 deletions
+36
View File
@@ -1,3 +1,39 @@
## v0.88.0 — controller-driven escrow ceremony: --output=json + localapi job + one-shot R claim (2026-07-13)
The agent half of the customer-facing recovery-code wizard (controller v0.127.0; every mechanism
validated by felhom.eu SPIKE-controller-escrow-2026-07-13 — PTY-under-no-TTY, fixed-argv sudoers
5/5 refusals, R pipe round-trip, env_reset, 2.32.4 s timings). Operator ruling F1 (2026-07-13):
R transiting the CF tunnel once at display is an accepted, documented risk (threat-model paragraph
in RUNBOOK-escrow-ceremony.md).
- **`--output=json` machine mode** (`cmd/felhom-agent/main.go`): `runSelftestEscrowCreate`'s body
extracted into the shared `escrowCeremony()` core; text mode stays BYTE-IDENTICAL (banner, R
block, exit codes 0/1/2, upload-fail-after-R order). json mode emits ONE
`escrow.CeremonyOutput` object on stdout (version 1: recovery_code, key_fingerprint,
entropy_bits, blob/identity sizes, restic_pw_sealed, uploaded), every human line to stderr, no
partial JSON on failure; `--offline`/`--paperkey` are refused in json mode (print-oriented).
- **The ONE fixed argv** (`internal/escrow/ceremony.go`): `escrow.CeremonyBinary` +
`escrow.CeremonyArgs()` — the single source shared by the localapi exec, the capability
manifest entry, and (byte-identically) the new `FELHOM_ESCROW` sudoers alias
(`configs/felhom-agent.sudoers`). `TestEscrowCeremonyArgvPinned` +
`TestManifestCoveredBySudoers` transitively lock runner == manifest == sudoers; never build the
argv with flag helpers, never normalize `--``-` (spike §2.2).
- **localapi ceremony endpoints** (`internal/localapi/escrow_ceremony.go`, `withGuest`-wrapped):
`POST /escrow/ceremony` (single-flight 409; detached job, 60 s timeout; `sudo -n` + the fixed
argv; stdout parsed then zeroed — SECRET-BEARING, never logged), `GET /escrow/ceremony/status`
(non-secret summary + stderr-tail failure detail ≤500; R structurally absent from the job
struct), `POST /escrow/ceremony/claim` (ONE-SHOT: 200 `{recovery_code}` once → holder zeroed;
410 on re-claim; **TTL 10 min**`unclaimed_void`, active AfterFunc belt + lazy check),
`GET /escrow/preflight` (storage id, DR tier, age, hub target, staged-secret informational,
`sudo -n -l` grant list-probe). Crash-safety is IN-MEMORY BY DESIGN — an agent restart loses R
safely (re-run supersedes); no journal, deliberately.
- **Capability** `escrow-ceremony` (Critical, `GatedBy: pbs_dr` EXPLICIT — non-pbsdr name by
decision): list-mode probe of the shared argv; inactive (never red) while the DR tier is off.
- Tests: one-shot claim + double-claim 410, TTL void + zeroed holder, R-substring absent from
every status/snapshot payload (incl. the serialized job struct), single-flight, supersede on
re-run, failure taxonomy (exit/unparseable/version), preflight truth table, argv pin. §10
red-proofs demonstrated (see felhom.eu REPORT).
## v0.87.0 — SystemDisks device-mapper walk: legacy-boot hosts get a working drive wizard (IA finding 2, MEDIUM) (2026-07-13)
On a legacy-boot PVE (LVM root, no mounted ESP) `SystemDisks` resolved NOTHING — `wholeDiskOf`