burn-down night: hub v0.138.0 source (R-879 seal + roll-back, R-136, R-283, R-349/R-276 hub halves), gates R-315/R-422 closed, R-325/R-426 partial, decisions 132-133, 05 §16 (195 -> 193)
gates / gates (push) Successful in 2m6s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 21:48:25 +02:00
parent 0a460bcdea
commit 8925dcc633
6 changed files with 90 additions and 16 deletions
@@ -419,7 +419,8 @@ Hungarian „öt szó" is correct and unchanged.
### 16.1 Form protection (CSRF) on both login paths (R-135) ### 16.1 Form protection (CSRF) on both login paths (R-135)
The operator logs in two ways: a browser session (`hub_session` cookie + a per-session token on every form) and HTTP The operator logs in two ways: a browser session (`__Host-hub_session` cookie since v0.138.0, R-136 — always Secure, `Path=/`, no Domain, so a sibling
subdomain cannot plant one; + a per-session token on every form) and HTTP
Basic for scripts. Until v0.135.0 a state-changing request with NO cookie skipped the token check — on the reasoning Basic for scripts. Until v0.135.0 a state-changing request with NO cookie skipped the token check — on the reasoning
that it must be a script. It need not be: a browser caches Basic credentials per origin and resends them on a that it must be a script. It need not be: a browser caches Basic credentials per origin and resends them on a
cross-site form POST (SameSite does not govern the Authorization header). Now a request without a session passes cross-site form POST (SameSite does not govern the Authorization header). Now a request without a session passes
@@ -439,9 +440,18 @@ Legacy rows are sealed in place at start-up (`SealLegacyRecoverySecrets`, measur
refused; a wrong key → a reveal is a 500 with nothing in the body or the log. Both retrieval paths (the operator page and refused; a wrong key → a reveal is a 500 with nothing in the body or the log. Both retrieval paths (the operator page and
the global-key API) open through `GetHostRecoveryCredential`, so the break-glass route still works with the UI down. the global-key API) open through `GetHostRecoveryCredential`, so the break-glass route still works with the UI down.
**What a copy of `hub.db` still holds readable** (R-879): each box's hub API key, each household's owner passphrase **Since v0.138.0 (R-879) four more columns are sealed the same way:** each box's hub API key (`hosts.api_key`), each
(`customer_configs.retrieval_password`) and API key, and the PBS-DR token values (`host_pbs_secrets.value`, kept after household's controller API key and owner passphrase (`customer_configs.api_key`, `retrieval_password`) and the PBS-DR
use). **And the key is the other half:** a backup of the database restores a hub that can open the sealed columns only token values (`host_pbs_secrets.value`). Hashing or deleting was not possible — every one of these values is served again
(re-enroll returns the key, the config ships the controller key, the operator page shows the passphrase, a PBS token can
be re-staged). The two API keys also carry an UNKEYED SHA-256 twin (`api_key_hash`, backfilled at every start), and a box
is looked up by that hash — **so a box authenticates even when the sealing key is missing or wrong** (`TestR879_BoxAuthSurvivesFailedSealing`).
Legacy rows are sealed at start-up (`SealLegacyBoxSecrets`, idempotent, non-fatal). A value that does not open marks the
record unreadable: its serve paths answer 500, a save refuses it (never blanks it), a PBS token is not consumed. **Roll-back
to a hub before v0.138.0** needs the columns opened first: `felhom-hub -unseal-box-secrets` (all or nothing; exit 1 =
nothing changed) — run in the running pod, then put the old image back. `guests.api_key` is an unused, always-empty
column and is not sealed. *Decided by CC unattended — operator may reverse (`09` §3 decision 132).*
**And the key is the other half:** a backup of the database restores a hub that can open the sealed columns only
with the same `OFFSITE_SECRET_KEY`; today that key exists only on DooPlex (the k8s Secret, and the GPG secrets export with the same `OFFSITE_SECRET_KEY`; today that key exists only on DooPlex (the k8s Secret, and the GPG secrets export
on the same machine). The off-site plan for the database and its key: `runbooks/RUNBOOK-hub-db-offsite-backup.md` on the same machine). The off-site plan for the database and its key: `runbooks/RUNBOOK-hub-db-offsite-backup.md`
(R-173 — option A decided 2026-10-05, `09` decision 125; §16.3). (R-173 — option A decided 2026-10-05, `09` decision 125; §16.3).
@@ -913,6 +913,19 @@ its length, and both fixes cost something the household would notice — operato
(a 75 GiB drill box) and changes a behaviour operators rely on, with no measured floor behind 120. **Chosen (a)**; (a 75 GiB drill box) and changes a behaviour operators rely on, with no measured floor behind 120. **Chosen (a)**;
reversible by renaming back. Whether a REAL floor exists is unmeasured. Installer 1.32.0 (`installer-v1.32.0`, not cut). reversible by renaming back. Whether a REAL floor exists is unmeasured. Installer 1.32.0 (`installer-v1.32.0`, not cut).
132. **A hub with no sealing key, asked to write a NEW box secret (R-879).** Options: (a) refuse — enrolment, customer
creation, passphrase regeneration and a PBS mint fail until the key is back, existing boxes unaffected; (b) write it
in plaintext — silently re-creates the readable secret the row removes. **Chosen (a)**: `05` §16.2 already says
„no key → a save is refused" for every sealed column, and the production hub runs with the key. The box lookup is
an UNKEYED hash so that authentication never depends on the key. Hub v0.138.0.
133. **What removing a household's own off-site target does to its repository password (R-729, R-545).** Options: (a)
shred it always — a hub-held package then hits the R-241 mint refusal, and history on the NAS loses its on-box key;
(b) refuse the whole press while the hub holds a package — refused on practically every box, R-729 unsolved; (c)
clear the target, SSH key and known-host line always; keep the password whenever anything could depend on it (a
hub package, escrowed, a successful run, snapshots), delete it otherwise. **Chosen (c)**: R-241's rule (never drop
a key a package protects) and „never the repository"; keeping a key is reversible, deleting it is not. Cost: on
most boxes the 0600 password file stays with no target. `07`. Controller (next release).
### 2026-10-05 (~21:00) — three rulings, the reviewer's picks given to the operator (recorded before the work; the burn-down night) ### 2026-10-05 (~21:00) — three rulings, the reviewer's picks given to the operator (recorded before the work; the burn-down night)
128. **R-126 — a `.fab` export onto a network drive.** **Refuse an export WITHOUT a password to any network drive; with 128. **R-126 — a `.fab` export onto a network drive.** **Refuse an export WITHOUT a password to any network drive; with
@@ -32,3 +32,15 @@ cherry-picks onto `main`, writes CHANGELOG, closes rows, pushes and watches CI.
| R-179 | fixed on main (NAS units; data-drive units left, GL6-F2) | 25 | `c11d4fbf` | | R-179 | fixed on main (NAS units; data-drive units left, GL6-F2) | 25 | `c11d4fbf` |
| R-310 | fixed on main + runbook line | 15 | `b43705ac` | | R-310 | fixed on main + runbook line | 15 | `b43705ac` |
| R-274 | fixed on main (disclosure + pinning tests; check shipped with R-297) | 15 | `f790734e` | | R-274 | fixed on main (disclosure + pinning tests; check shipped with R-297) | 15 | `f790734e` |
| R-879 | fixed (hub v0.138.0) + roll-back command; decision 132; close after deploy | 75 | `5f060e3d`, `91e4ace9` |
| R-136 | fixed (hub v0.138.0); close after deploy | 20 | `e221476f` |
| R-283 | fixed (hub v0.138.0): box's own claim state shown; un-claim NOT taken (operator's call) | 30 | `daeed175` |
| R-540 | moved to D (multi-box design + money) | 10 | — |
| R-435 | moved to D (per-app counts, two repos, unmeasured threshold) | 10 | — |
| R-315 | fixed and closed (gate tooling is live on push) | 25 | `900e6d86` |
| R-325 | felhom.eu half fixed; controller half → helper ctrl-c | 15 | `7ee00d59` |
| R-422 | fixed and closed | 25 | `bf179847` |
| R-426 | partial (20 → 16 exemptions); row stays open | 15 | `68df84bc` |
| R-502 | moved to A (a Docker-running gate on DooPlex needs an operator word) | 10 | — |
| R-349 | hub half fixed (hub v0.138.0) | 20 | `a5b4d29a` |
| R-276 | hub half fixed (hub v0.138.0) | 15 | `0a460bcd` |
+9
View File
@@ -26,6 +26,15 @@
--- ---
## 2026-10-05 (night) — the burn-down night: gate fixes
The full text of every row below: `git show 86def579:documentation/backlog/OPEN-ITEMS.md`.
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-315** | **The wire-contract gate's positive control FAILS on the new wire: it checks name-presence, not decodability.** (P4) | CLOSED 2026-10-05 — FIXED (the burn-down night): mirrored roots are checked field by field | felhom.eu `900e6d86`: `scripts/wire_contract_gate.py` checks the receiver's mirror type for the hub→agent escrow/retained and hub→controller escrow-ACK roots and prints each root's check; decoys `wire-mirror/renamed` (convicts — the measured `superseded_at` rename) and `wire-mirror/genuine` (passes) in `scripts/test_gate_decoys.py`; red-proof: deleting the MIRRORS entry lets the renamed decoy pass. The two report wires stay name-reachability (printed as such). |
| **R-422** | **`reuse_refs_check.py` only checks citations whose extension is one of `go py html css yml yaml sh`.** (P4) | CLOSED 2026-10-05 — FIXED (the burn-down night): .md citations are checked | felhom.eu `bf179847`: `scripts/reuse_refs_check.py` checks `.md` citations; audits/ and documentation/tests are indexed for .md only; walk across all four repos' REUSE.md: 0 failures after one fix; tests `scripts/test_reuse_refs_check.py` (3) + the KNOWN-HOLE decoy now expects a conviction; red-proofs recorded in the night log. |
## 2026-10-05 (night) — the burn-down night: stale rows ## 2026-10-05 (night) — the burn-down night: stale rows
The full text of every row below: `git show c8da8e04:documentation/backlog/OPEN-ITEMS.md`. The full text of every row below: `git show c8da8e04:documentation/backlog/OPEN-ITEMS.md`.
File diff suppressed because one or more lines are too long
+32
View File
@@ -1,3 +1,35 @@
## v0.138.0 — secrets sealed at rest, a cookie no sibling can plant, the box's own claim state, agent-binary drift (burn-down night: R-879, R-136, R-283, R-349, R-276) (2026-10-05)
**Operator action on deploy: log in again once** (the session cookie is renamed). **Roll-back needs one extra step
first:** `felhom-hub -unseal-box-secrets` in the running pod (see R-879 below and `05` §16.2), then the old image.
- **R-879 (security):** the box API keys (`hosts.api_key`), the controller API keys and owner passphrases
(`customer_configs.api_key` / `retrieval_password`) and the PBS-DR token values (`host_pbs_secrets.value`) are sealed at
rest with the R-821/R-133 seal (`OFFSITE_SECRET_KEY`). The API keys get an `api_key_hash` lookup column (SHA-256,
backfilled at every start without the key), so a box authenticates even when the sealing key is missing or wrong;
legacy rows are sealed at start-up by `SealLegacyBoxSecrets` (idempotent, non-fatal). A sealed value that does not open
marks the record unreadable: the config/recovery/artifacts/enroll/appliance paths answer 500, saves refuse it (no
blanking), and a PBS token is not consumed. No key: new secrets are refused, never written in plaintext (`09` §3
decision 132). Tests `TestR879_*` (store, api, configgen, cmd/hub); six red-proofs.
- **R-879 roll-back:** `felhom-hub -unseal-box-secrets` (same `-config` and `OFFSITE_SECRET_KEY` as the server) opens the
four columns back to plaintext and exits — all or nothing (exit 1 = a value did not open, nothing changed; exit 2 = key
missing), idempotent, counts only in the log; `api_key_hash` is kept, so 0.137.0 can be put back.
- **R-136 (security):** the operator session cookie is `__Host-hub_session` — always Secure, `Path=/`, no Domain — so a
sibling subdomain cannot plant a session cookie the hub would read first. Every operator is logged out once; a browser
login over plain HTTP no longer keeps a session (accepted); Basic auth for scripts is unchanged.
- **R-283:** the customer page's claim card shows the box's own claim state from its latest report (claimed / not claimed
yet / unknown), with an amber warning when the hub says claimed and the box says not — the shape a guest rebuild
leaves. Nothing un-claims: the hub's claim stays set-only by design.
- **R-349 (hub half):** the host page compares the agent's reported `agent_sha256` (the running binary, agent R-349) with
the vouched agent sha when the box reports the vouched agent version — „matches vouched" or an amber DRIFT naming both
hashes. Empty hash (older agent) = unknown, never drift.
- **R-276 (hub half):** deleting a host asks the WireGuard peer sync for an immediate full-list push (as the customer
delete does since R-600), so the off-site endpoint drops the removed box's peer at once instead of on the next tick.
- **scripts:** `wire_contract_gate.py` checks the mirrored escrow roots field by field and prints each root's check
(R-315); `hub_copy_gate.py` accepts the controller gate importing the shared vocabulary (R-325, felhom.eu half);
`reuse_refs_check.py` checks `.md` citations (R-422); the instructions gate's decoy is in the suite, exemptions 20 → 16
(R-426, partial).
## v0.137.0 — small fixes from the burn-down: right numbers, right flashes, one create per press, the delete says when it opens (R-277, R-581, R-600, R-544, R-855, R-134, R-92, R-292, R-599, R-725, R-728, R-208; R-124 fixture) (2026-10-05) ## v0.137.0 — small fixes from the burn-down: right numbers, right flashes, one create per press, the delete says when it opens (R-277, R-581, R-600, R-544, R-855, R-134, R-92, R-292, R-599, R-725, R-728, R-208; R-124 fixture) (2026-10-05)
**Operator action on deploy: none.** One new operator flash (`artifact_version_missing`) and one new customer i18n key **Operator action on deploy: none.** One new operator flash (`artifact_version_missing`) and one new customer i18n key