docs: the offsite key is proven recoverable (R-199 closed, R-200 half, R-201 scoped)
gates / gates (push) Successful in 8s

This commit is contained in:
2026-08-04 13:55:25 +02:00
parent dd089265e8
commit 846253baa8
8 changed files with 272 additions and 212 deletions
+35
View File
@@ -17,6 +17,41 @@
## Standing rulings
**S-29 — a box may fetch its OWN sealed recovery blob with its OWN credential; the operator-driven DR
path is a separate thing and stays gated (2026-08-04, R-199; hub v0.94.0 + agent v0.125.0 + controller
v0.195.0).**
**The decision, so it is never re-litigated from scratch.** Self-service recovery needs the box to
obtain its own sealed package. The only retrieval that existed was operator-gated behind recovery mode
and the global key (`dr.go`), which cannot serve a customer-present flow. `GET /hosts/{id}/escrow` now
serves that ONE object to its authenticated owner.
- **Why it is safe:** the blob is opaque; the hub has never held R and has no decrypt path; a wrong
code fails closed at age's scrypt KDF (measured live — `exit status 1`, no plaintext, nothing
written); the caller already authenticates as this host for its report, desired state, WG
registration and PBS token, so this adds no new identity, only a new object.
- **What it changes, which is the whole of the trade:** obtaining the blob used to require the
OPERATOR to arm recovery mode. It now needs only that box's credential. **Fewer parties are
required.** The mitigation is that the capability is AUDITED, not silent: every retrieval raises
`escrow_blob_served` (warning, operator-only), written before the bytes leave.
- **The override is one constant.** `api.escrowSelfServiceRetrieval` — false re-imposes the
recovery-mode requirement on this endpoint and changes nothing else. Do not scatter the condition.
- **`handleReEnroll` / `handleGetRestoreDirective` are UNTOUCHED** and must stay so: they rotate the
host API key and serve the K-escrow and the directive as well. Different object, different gate.
Pinned by `TestEscrowGet_OperatorDRPathUnchanged`.
**Facts a future session needs before touching this area:**
- **The unseal lives in the AGENT and must stay there.** `age` is an agent runtime dependency
(`/usr/bin/age`, present on both demo hosts) and is deliberately absent from the controller image;
the blob is host-scoped and its only writer is the agent under the per-host key.
- **The agent returns ONE field.** Not the tunnel token, not the PBS token, not the WG key — the
controller is a trust tier down. Narrowing cost nothing and is not recoverable later.
- **R's rules:** in memory for one call, cleared on every path, never on disk, never in argv, never
logged, never echoed. The controller CLI reads it from STDIN for exactly that reason. Verified live:
0 log lines, 0 files, 0 leftover staging dirs, with a positive control proving the search worked.
- **The chain today: links 18 walked, 911 not.** The KEY comes back. Nothing installs it, reopens a
repository with it, or restores a file — R-200's remaining half and R-201.
**S-28 — the escrow retention now covers the OFF-SITE data key, and customer-present recovery is the
accepted design, which makes that retention load-bearing (2026-08-04, R-198/R-197; hub v0.93.0).**