Files
felhom.eu/REPORT.md
T
admin 3f2b7bc023
gates / gates (push) Successful in 7s
RECON: trace the offsite DR chain link by link — it does not join up (R-198..R-201)
Read-only recon of the escrow -> recovery chain, from a dead node to an open
repository. No production code, no build, no version bump.

Headline: the hub's superseded-escrow retention does NOT retain the offsite
repository password. host_escrow_superseded has no identity_blob column and
demoteCurrentEscrowTx copies only the K-escrow blob, so what survives a
supersession is the PBS datastore key, not the restic repo password. The next
escrow ceremony -- which the system tells a rebuilt box's customer to run --
destroys the last copy. Both demo boxes crossed that line on 2026-08-04.

Also established:
- the hub's blob-serving endpoints (re-enroll / restore-directive) have zero
  callers anywhere: agent, hub UI, scripts, runbooks (R-199)
- POST /backup/offbox/inject-password is routed and handled but no template
  contains the form (R-200)
- nothing in the recovery path has ever been exercised; the one live
  round-trip proof (2026-06-10) predates the ResticRepoPassword field (R-201)
- a fail-closed mint refusal IS implementable: the report ACK already carries
  escrow{identity_blob_present, restic_pw_sha256} and the controller discards
  it whenever no offbox target exists

Corrections: yesterday's spike annotated (candidate (b) overturned in part --
unattended recovery is impossible, customer-present is not); capability-map
retention claim struck through and replaced with what the code does.

Deliverable: documentation/audits/RECON-offsite-dr-chain-2026-08-04.md
Register: new R-198..R-201; R-193 and R-192 updated; STATUS.md refreshed.
2026-08-04 12:16:04 +02:00

122 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — RECON: can a customer actually get their backups back? (2026-08-04)
**Class:** recon / spike. **No production code. No build, no deploy, no version bump.**
**Baselines re-read on arrival:** `felhom-agent` `856a127cd60b` (v0.124.1) · `felhom-controller`
`0887fd676d60` (v0.194.0) · `felhom.eu` `d26f49ad680e` (hub v0.92.0). All three trees clean.
> Supersedes the earlier 2026-08-04 report (R-193/R-192 spike + R-195), per the overwrite convention.
> Nothing durable from it lived only here — it is recorded in `hub/CHANGELOG.md`, `OPEN-ITEMS.md`
> R-192/R-193/R-195/R-196/R-197 and `audits/SPIKE-offsite-credential-recovery-2026-08-04.md`.
**Deliverable:** `documentation/audits/RECON-offsite-dr-chain-2026-08-04.md` — Q1Q8 with method,
measurement and ruling per question, the eleven-link chain table, the drill design, every query used,
and the operator's open decisions.
---
## 1. What was established
**The chain from a dead node to an open repository is not assembled, and the failure is deeper than
"some links are manual".**
1. **Links 14 are PROVEN-LIVE** — mint → stage to the agent → seal under R → store on the hub.
2. **Link 5 is lossy, and this is the finding of the session.** `host_escrow_superseded` has **no
`identity_blob` column**, and `demoteCurrentEscrowTx` (`hub/internal/store/store.go:2547-2556`)
copies only the K-escrow `blob`. The **restic repository password lives in `identity_blob`** — so
the retention feature built for the reinstall-orphan incident does not retain the secret that
incident is about. The **next escrow ceremony destroys the last copy**, and that ceremony is exactly
what the system tells a rebuilt box's customer to run. Both demo boxes crossed that line on
2026-08-04 at 07:15:36 and 07:20:08. → **R-198**
3. **Link 6 has no client anywhere.** `handleReEnroll` / `handleGetRestoreDirective` serve the blobs;
zero callers in the agent, the hub UI, any script or any runbook. Retrieval is `sqlite3` by hand on
the hub pod. → **R-199**
4. **Links 78 are manual-only.** `UnwrapIdentityBundle`'s only production caller is
`--selftest=identity-consume`, reading R from `FELHOM_RECOVERY_CODE`; nothing extracts
`restic_repo_password` from the recovered bundle.
5. **Link 9's form does not exist.** `POST /backup/offbox/inject-password` is routed, handled and
unit-tested; **no template contains it.****R-200**
6. **Link 11 has never happened.** No blob served, no fork-4 bundle unsealed with a real R outside a
unit test, no recovered password injected, no repo reopened, no restore. The one live round-trip
proof (2026-06-10) **predates the `ResticRepoPassword` field by a month**. → **R-201**
**And the most useful thing settled: a fail-closed refusal IS implementable.** The hub already sends
every box `escrow{identity_blob_present, restic_pw_sha256, created_at}` on every report ACK
(`api/handler.go:504-510`), and the controller **discards it** whenever no offbox target exists
(`report/escrow_confirm.go:75-84`). Persisting it needs no new hub API and no new secret; the
`ClaimSync` set-only pattern is the precedent.
**The operator's ruled recovery-screen shape is buildable** and is priced row by row in §9 of the
findings doc: fresh-install signal exists (the mint branch's own `os.Stat`); hub-has-a-blob exists on
the wire; the preview is cheap and genuinely read-only (`restic snapshots --json` +
`stats --mode raw-data --json` are already the box's own counters); the one real cost is crossing
agent→controller for the unseal, because the controller image ships no `age` — one new agent local-API
endpoint mirroring `/escrow/ceremony/claim`, plus a narrow hub `GET /hosts/<id>/escrow`.
## 2. Corrections owed and made
- **Yesterday's spike annotated, not rewritten** (`audits/SPIKE-offsite-credential-recovery-2026-08-04.md`):
a dated block at the top plus an inline note on Q8(b). Its candidate (b) verdict *"not implementable
against a zero-knowledge escrow"* is **too strong** — unattended recovery is impossible; a
customer-present recovery is not. Everything else in that spike stands, and its Q3 remains unmeasured.
- **Capability map corrected** (`architecture/00-capability-map.md`, escrow-ceremony row): the hub
v0.60.0 retention claim *"a previous passphrase stays recoverable with its recovery code"* is struck
through and replaced with what the code does — K-escrow only, no read path, `ListSupersededEscrow`
has zero production callers.
- **The same false promise is made to the customer in Hungarian** on the orphan card
(`controller/internal/web/templates/backups_remote.html:66,69`). Recorded in R-198; **not edited**
this session ships no controller change.
- **The prompt's Q4 premise is a conflation, and it is corrected in the doc:** `escrow_confirm.go`
**does** fire on a mint (pending branch, hash mismatch, one WARN). The "wired to the wrong path"
finding was about the hub's `escrow_stale` event (R-196), a different mechanism. What is missing is
not the detection but the consequence — the warning reaches a log file and prescribes the act that
closes the recovery window.
## 3. Register
- **New: R-198, R-199, R-200, R-201** (highest previous ID was R-197; grepped across `OPEN-ITEMS.md`,
`ROADMAP.md`, `STATUS.md`, `CONTEXT.md` before minting).
- **Updated: R-193** (seven-part addendum: (b) overturned in part, the chain census, the retention
finding, fail-closed implementable, the operator ruling priced, the drill, Q3 still unmeasured) and
**R-192** (two inputs, no verdict change).
- **`STATUS.md`** updated in operator language: two new "what's broken" entries, three new
"waiting on you" items, a "changed since" line.
## 4. Live measurements taken (read-only)
Hub SQLite snapshot with the `-wal` (`kubectl exec … cat`), `PRAGMA integrity_check``ok`, freshness
proven positively (`MAX(host_reports.received_at)` = `2026-08-04 10:00:40` vs `datetime('now')` =
`10:03:48`). Escrow tables, `PRAGMA table_info` on both, latest-report `$.offsite.*` and `$.claimed`
per customer, the `claimed` 1→0→1 transition across both rebuilds, and the events table since
2026-08-03. Plus one `ls -la --time-style=full-iso` inside guest 9201 proving the data dir, the
`encryption.key` and `offbox/repo_password` are all newborn as of `2026-08-03 07:17:25``07:18:02`.
Every query with its field names and values is in §11 of the findings doc.
**Nothing was written, run, rotated, re-issued, deleted or reconfigured anywhere.** No ceremony, no
`selftest=identity-consume`, no touch on the storage endpoint.
## 5. What could not be established
The 2026-08-05 ~02:15 UTC run's outcome (still the decisive measurement; neither box has run —
`last_run` absent on both); whether demo-hp's data dir was fresh (measured only on demo-felhom;
demo-hp needs the break-glass credential this read-only session did not use); whether the orphaned
ciphertext still exists at the provider; whether `age` is present on either demo host. Named in §12
of the findings doc.
## 6. Observations recorded, not acted on
A fresh `encryption.key` on every rebuild makes every pre-rebuild `app.yaml` `ENC:` secret
undecryptable — circular dependency **C-1** of `_recovery-inventory-2026-07-28.md`, which fired live on
both demo boxes this week and is a strictly larger blast radius than the offsite repo. `Claimed`
returning to 1 with no event means "the customer claimed this box" has no audit trail.
`offboxRecordStats` persists `RepoSizeBytes` as a side effect of counting. `handleReEnroll` returns a
freshly minted API key in its response body on an endpoint nothing currently calls.
## 7. Conventions
- **No `CHANGELOG.md` entry.** `felhom.eu` has no root changelog — they are per-area (`hub/`,
`scripts/`, `website/`) and log code changes. This session changed none of those areas. Stated
rather than silently skipped.
- **`REUSE.md`** untouched — no shared helper or pattern moved.
- **Gates + CI run id** are recorded in the session summary.
</content>