5.0 KiB
REPORT — v0.127.0: customer-facing escrow ceremony wizard + Scenario-F stale-blob re-check
Date: 2026-07-13 · Version: controller v0.127.0 (from v0.126.4, baseline 51c871ad confirmed) · MinAgent: 0.88.0 (wizard only — everything else runs on older agents). Commit 08a966b on main. Spike basis: felhom.eu SPIKE-controller-escrow-2026-07-13. Operator ruling F1 (2026-07-13): R over the CF tunnel once at reveal = accepted risk (threat model in felhom.eu RUNBOOK-escrow-ceremony.md).
What shipped
- Wizard
/backup/escrow(web/escrow_handlers.go,templates/backups_escrow.html): preflight → warnings (+supersede variant) → password re-auth → run (2 s poll) → ONE-SHOT reveal (no-store; R only ever in the claim XHR + the page's JS scope) → typed-back (two random words, client-side) → finish; void/410 state honest./api/escrow/{preflight,start, status,claim}behind session auth + CSRF (mux, the disk-API pattern). - Start order (load-bearing): re-auth (login rate limiter) → re-stage-first (abort on failure — no hash-less blob can be minted from the UI) → version gate (≥ 0.88.0, header-absent = older, fail-closed) → trigger. Each refusal exits with the agent untouched.
- Scenario F (
report/escrow_confirm.go): escrowed-state hash re-check on every ACK — mismatch OR hash-less blob → in-memory stale flag (card warning + re-ceremony CTA) + one WARN per distinct hub hash; match/auto-confirm clears; NEVER flips state, NEVER blocks runs. - Card rework (
backups_remote.html): manual-confirm BUTTON removed (endpoint stays, deprecated); pending/stale/clean/agent-too-old states per spec copy. All 4 UI gates green (template_id, emoji, native-confirm, offbox-rename).
Green gate + red-proofs (run → fail → revert)
go build && go vet && go test ./... green at every phase; all four UI gates green. Test
functions 489 → 502 (+5 stale table, +8 wizard/render). Red-proofs:
4. Reconcile reverted to early-return-when-not-pending → all four TestEscrowStale_* FAILED.
5. re-auth compare bypassed (compiling mutation) → TestEscrowStart_SecurityGates/wrong_password
/rate_limitedFAILED (agent got called / counter untouched). (1/2/3/6 are agent-side — felhom-agent/REPORT.md.)
Deployed + live-validated (2026-07-13 evening)
| Check | Result | Method |
|---|---|---|
| demo guest 9201 | 0.127.0 Up (healthy) |
docker ps |
| drill guest 9201 (VM qm 300) | 0.127.0 Up (healthy) |
docker ps |
| Scenario F fires LIVE (drill) | first ACK after deploy (17:08Z): [WARN] [escrow-confirm] STALE escrow: the hub's current blob carries NO password hash (hash-less supersession)… — the EXACT state the spike left; state stayed escrowed, runs unblocked |
controller log |
| Scenario F fires LIVE (demo) | same warn 17:07:42Z on the demo's legacy hash-less blob — honest per the truth table; fix = one wizard run (Viktor's login; operator follow-up) | controller log |
| Ceremony repairs the blob | drill: stage-first (real secret) → agent job → hub restic_pw_sha256=24a887bafce4… == sha256(local repo password) |
endpoint-exact + hub DB read-only |
| Stale flag CLEARS | the 17:23:01Z ACK (first after the covering blob) processed with ZERO escrow-confirm output; escrow_state stayed "escrowed" in settings.json. The hub verifiably serves the covering hash, so the match path (the only branch for it) cleared the flag — the clear itself is silent by design and pinned by TestEscrowStale_MatchClearsFlag |
controller log + settings.json + hub DB |
| One-shot semantics | claim 1 = 200 (10-word R, never printed) → claim 2 = 410 → status claimed | endpoint-exact |
| Wizard route + gate | GET /backup/escrow unauth → 302 /login?next=%2Fbackup%2Fescrow via the real in-guest traefik |
curl through the guest |
Method disclosure + NOT yet live-validated: the drill box's dashboard password is CUSTOMER-owned (Viktor set it at the take-two claim), and the wizard's re-auth requires typing it — so the FULL browser wizard leg (login → preflight UI → re-auth → reveal → typed-back → finish) could not be driven autonomously and remains NOT live-validated; it needs one supervised pass with Viktor (5 minutes — also the demo box's own wizard run to clear ITS stale warning). Every layer beneath it was validated endpoint-exact with the real token over the real pinned channel (the exact calls the handlers make), plus full unit coverage of the handler order/gates. Also not live-validated: a real customer box (Peti), TTL expiry in real time (clock-jump-tested).
Observations
- The demo's stale warning is a FEATURE surfacing a real pre-existing gap (its blob never covered the current password); expect the card warning on demo until a wizard run.
AgentVersion()is populated by ANY agent traffic; the wizard page/preflight naturally warm it before the start gate — a cold controller straight to POST /api/escrow/start could see "" and 409 until first traffic (self-heals; worth remembering when scripting).