hub v0.94.0: a box can fetch its own sealed recovery package (R-199 link 6)
gates / gates (push) Successful in 7s
gates / gates (push) Successful in 7s
Link 6 of the recovery chain had no client. The hub has served the identity blob since
slice 10D from handleReEnroll / handleGetRestoreDirective, gated on operator-armed recovery
mode and the global key -- and nothing in the agent, the hub UI, any script or any runbook
ever called either. The only documented retrieval was sqlite3 writefile() by hand on a
kubectl cp-ed database.
GET /api/v1/hosts/{host_id}/escrow is the box-authenticated mirror of the PUT that put the
blob there. Self-scoped (a per-host key reads only its own; global may read any). A host with
no bundle gets 200 {present:false} -- a 404 is indistinguishable from an unknown host and a
bare empty 200 from a zero-length blob.
THE TRADE IS RECORDED IN THE HANDLER, not inferred: obtaining the blob used to require the
operator to arm recovery mode; now whoever controls a rebuilt box can obtain it with that
box's own credential. They still cannot open it -- the hub has never held R and a wrong code
fails closed at age's scrypt KDF. The mitigation is that every retrieval raises
escrow_blob_served (warning, operator-only), recorded before the bytes leave.
escrowSelfServiceRetrieval is the single decision point: flip it to false and the endpoint
additionally requires recovery mode, changing nothing else.
The operator-driven DR path is untouched, pinned by a test. Red-proofs observed: removing the
ownership check serves host B's blob to host A; removing the record makes it silent.
This commit is contained in:
@@ -1,3 +1,36 @@
|
||||
## v0.94.0 — a box can fetch its own sealed recovery package (2026-08-04, R-199 link 6)
|
||||
|
||||
**Chain link 6 had no client.** The hub has served the identity blob since slice 10D, from
|
||||
`handleReEnroll` / `handleGetRestoreDirective` — gated on operator-armed recovery mode and the global
|
||||
key. Nothing in the agent, the hub UI, any script or any runbook ever called either. The only
|
||||
documented retrieval was `sqlite3 … writefile(…)` by hand on a `kubectl cp`-ed database.
|
||||
|
||||
**`GET /api/v1/hosts/{host_id}/escrow`** — the box-authenticated MIRROR of the PUT that put the blob
|
||||
there. Self-scoped: a per-host key reads only its own; the global key may read any, the same asymmetry
|
||||
the PUT has. A host with no sealed bundle gets `200 {present:false}` — a clean answer, because a 404
|
||||
is indistinguishable from an unknown host and a bare empty 200 from a zero-length blob, and neither
|
||||
of those is what "no ceremony has run yet" means.
|
||||
|
||||
**THE TRADE, RECORDED IN THE HANDLER RATHER THAN INFERRED.** Before this, obtaining the blob required
|
||||
the OPERATOR to arm recovery mode. Now whoever controls a rebuilt box can obtain it with that box's
|
||||
own credential. That is a real reduction in the number of parties required, and it is the whole of the
|
||||
trade — they still cannot open it, because the hub has never held R and a wrong code fails closed at
|
||||
age's scrypt KDF. **The mitigation is that the capability is audited rather than silent:** every
|
||||
successful retrieval raises `escrow_blob_served` (warning, operator-only), recorded before the bytes
|
||||
leave. A silent capability on this object is the shape the last two weeks were spent removing.
|
||||
|
||||
`escrowSelfServiceRetrieval` is a single named constant — the §8.2/§8.3 decision point. Flipping it to
|
||||
false re-imposes the recovery-mode requirement on this endpoint and changes nothing else, so the
|
||||
operator can overrule the trade at the cost of a boolean rather than a redesign.
|
||||
|
||||
**The operator-driven DR path is untouched** — same gate, same behaviour, pinned by a test that
|
||||
exercises re-enroll and restore-directive with recovery mode off and on. Red-proofs observed: removing
|
||||
the ownership check makes a cross-host read succeed (host A served host B's blob); removing the audit
|
||||
record makes the retrieval silent.
|
||||
|
||||
**Not in this release:** the customer-facing flow. No card, no form, no preview — those are designed on
|
||||
ground that has been walked, and R-200/R-201 are that walk.
|
||||
|
||||
## v0.93.0 — the retention keeps the key it was built to keep, and three things stop lying (2026-08-04, R-198/R-197/R-196/R-192)
|
||||
|
||||
### R-198 — the superseded-escrow retention was preserving the wrong key, and the ceremony was destroying the right one
|
||||
|
||||
Reference in New Issue
Block a user