Files
felhom.eu/documentation/audits/night-burndown-2026-10-06/design-R-105.md
T

66 lines
5.1 KiB
Markdown

# R-105 — the two "empty DR records" — design proposal (burn-down night 2026-10-06, no code)
Baselines read: felhom.eu `8e2dc204` (hub v0.140.0 live), felhom-agent `74b5eae`. Architecture: `05-hub-architecture.md`
§9 and §11 (the "slim DR record"), `06-offsite-connectivity.md` §3.5 (the escrow directive).
## 1. The problem, and what was measured
The row says two hub-held records are `{}` on every box: `hosts.dr_record_json` and `host_escrow.directive_json`
(the third, `dr_recipe…drives`, was fixed 2026-07-28 and is not re-measured tonight).
**Tonight's reading is in SOURCE, not in the database** — reading a copy of the hub database was refused by the
session's permission check, so no value was read. Source proves more than a reading would: for these two fields the
empty value is the only value the shipped code can produce. The hub's host pages (read through the operator UI) show
"DR Recipe present / Key Escrow present" on demo-hp, demo-felhom and Tester 1, and "none / none" on Tester 2.
## 2. What the code does today (read in source)
- **`hosts.dr_record_json` has no writer and no reader.** Created by hub v0.7.0 (`7c0c7545`) with default `'{}'`
(`hub/internal/store/store.go:360`); scanned into `Host.DRRecordJSON` (`store.go:2795`, `:2867`) and read by
nothing else in the hub, the agent or the controller (repo-wide grep). The `05` §9 "slim DR record" was never built.
- **`host_escrow.directive_json` is written only by a by-hand flag.** The agent sends a directive only when the
operator runs `--selftest=escrow-create -directive <file>` (`felhom-agent/cmd/felhom-agent/main.go:201`,
`:2767-2771`, `:3132-3135`). The production ceremony (the customer's escrow wizard) runs ONE fixed argv with no
`-directive` (`felhom-agent/configs/felhom-agent.sudoers:284`). When that upload carries an identity blob, the hub
stores the missing directive as `'{}'` (`hub/internal/api/handler.go:1293-1305`, `store.go:3612`) — so every
wizard escrow writes `{}`, and also overwrites the one directive made by hand on 2026-07-04 (`06` §3.5).
- **Nothing reads the directive's fields.** The hub serves it only from `/re-enroll` and `/restore-directive`
(`hub/internal/api/dr.go:101`, `:155`); no agent or controller code calls either route (grep).
- **The DR path that IS built does not need them.** The agent's restore plan reads the desired-state
`restore_directive` plus the DR recipe (`felhom-agent/internal/dr/plan.go:45`, `:104`); the recipe carries the PBS
repo id and namespace (`internal/hub/dr_recipe.go` `DRPBSCoord`), the hub holds the endpoint's PBS fingerprint
(`hub/internal/tenantsync/client.go:136`), and the wrapped key is `host_escrow.blob`.
So the row's two thirds are not a fault in a running path. They are a **design that was half-built and then
bypassed**, and two architecture documents still describe it as if it existed.
## 3. Options
**A. Retire both, and correct the documents.** Drop the `DRRecordJSON` scan field; stop storing the directive
(the column stays, read as `{}`); `05` §9/§11 and `06` §3.5 say where each fact really lives (recipe, tenantsync,
escrow blob). Cost: ~45 min, hub only, no box change. Can go wrong: if a future re-enroll client wants the
directive, it must be rebuilt — the routes stay and serve `{}`.
**B. Populate the directive from the ceremony.** The agent fills `{pbs repo id, namespace, endpoint fingerprint}`
in the wizard ceremony. Cost: agent + sudoers argv change (the argv is pinned byte-for-byte) + a bundle delivery;
~2 h and a release. Can go wrong: a second copy of facts the recipe already carries, which can disagree with it —
the R-106 namespace defect was exactly such a disagreement.
**C. Do nothing more; close R-105 with this evidence.** Cost: nothing. The two documents keep describing a record
that does not exist, which is how this row was filed in the first place.
## 4. The pick — PROPOSAL for the operator, not a decision
**Option A.** One source per fact; the recipe is reported every cycle and was fixed to match the backup (R-106).
It removes a design promise in `05` §9, so it needs the operator's word (a design decision is not a defect).
Until then the row can move to WAITING-ON-OPERATOR: there is no data at risk — the empty fields have no reader.
## 5. First slice and its proof
- Red test first: a hub test that the escrow PUT with an identity blob and no directive leaves `directive_json`
unchanged from a hand-made value (today it overwrites with `{}` — fails); then decide by the option.
- A test pinning "no reader": an AST/grep test in the hub that `DRRecordJSON` is not referenced outside the store
(so a new reader cannot appear without this design being revisited).
- Live proof: none needed on a box (hub-only); after deploy, the host page still shows "DR Recipe present".
## 6. Open questions for the operator
1. Retire the "slim DR record" of `05` §9 (Option A)? If you do nothing: the fields stay empty and unread, the
documents stay wrong, nothing breaks.
2. Tester 2's host page shows no DR recipe and no key escrow. Expected for a box that never did the escrow wizard —
is that the case? If you do nothing: a Tester 2 host loss has no hub-held recovery material.