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)
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
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
@@ -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
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
(`customer_configs.retrieval_password`) and API key, and the PBS-DR token values (`host_pbs_secrets.value`, kept after
use). **And the key is the other half:** a backup of the database restores a hub that can open the sealed columns only
**Since v0.138.0 (R-879) four more columns are sealed the same way:** each box's hub API key (`hosts.api_key`), each
household's controller API key and owner passphrase (`customer_configs.api_key`, `retrieval_password`) and the PBS-DR
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
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).
@@ -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)**;
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)
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-310 | fixed on main + runbook line | 15 | `b43705ac` |
| 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
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