slice 10D (hub): DR capstone — recovery mode + re-enroll + directive serving (hub v0.11.0)

Recovery-mode toggle (global key, bounded auto-expiry) gates re-enroll +
restore-directive serving. Re-enroll rotates the agent<->hub credential to the
new box (old key revoked); returns the opaque escrow blobs + non-secret
directive. Store gains recovery_mode_until + identity_blob + directive_json.
Hub holds no usable secret + no Cloudflare write-power (operator-side rotation).
Doc 03 §9: slice 10 CLOSED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:48:38 +02:00
parent a22b87e6e3
commit 3457415117
7 changed files with 533 additions and 34 deletions
+34
View File
@@ -1,5 +1,39 @@
# Felhom Hub — Changelog
## v0.11.0 — slice 10D: DR capstone — recovery mode + re-enroll + directive serving (2026-06-10)
The hub half of the slice-10 DR capstone (closes slice 10). The hub ORCHESTRATES recovery but holds
**no usable secret and no Cloudflare write-power**: the escrow blobs it serves are opaque (need `R`,
which the hub never has), and the destructive tunnel/PBS rotation is the **operator's** step from a
trusted environment. A compromised hub can at most hand out opaque blobs + rotate/revoke its own
per-host credential — it cannot hijack a customer's tunnel.
### Added
- **`PUT /admin/hosts/{id}/recovery-mode`** (global key) — arm recovery mode with a bounded TTL
(`ttl_seconds`, clamped [60s, 4h], default 30m → **auto-expires**); **`DELETE`** to disable. The
restore directive + re-enroll are served ONLY while recovery mode is active.
- **`POST /hosts/{id}/re-enroll`** — gated ONLY on recovery mode (the lost box has no old key; the
operator armed recovery mode after out-of-band validation). Rotates the host's API key to the new
box's key (**the old box's hub access is revoked instantly**) and returns the DR directive + the two
**opaque** escrow blobs. Without recovery mode → 403. Zero-knowledge: even a wrongful re-enroll in
the window leaks nothing recoverable (the blobs need `R`).
- **`GET /hosts/{id}/restore-directive`** (re-enrolled key, recovery-gated) — re-fetch the directive.
- **Store/escrow**: `hosts.recovery_mode_until` (additive); `host_escrow.identity_blob` +
`directive_json` (the age-wrapped identity blob + non-secret directive, stored alongside the
K-escrow). Methods: `SetRecoveryMode`/`ClearRecoveryMode`, `RotateHostAPIKey`, `SaveHostDRBundle`/
`GetHostDRBundle`. The slice-7 escrow upload (`PUT /hosts/{id}/escrow`) now also accepts
`identity_blob_b64` + `directive` (additive).
### Not built (by design — the locked rotation model)
- **No Cloudflare write-credential in the hub.** The operator deletes the stale tunnel connector +
rotates the tunnel/PBS token from their trusted environment (a documented procedure / future small
operator CLI). The hub may optionally hold a read-only CF token to surface connector state.
### Tests
- re-enroll refused without recovery mode (403); recovery-mode arm is global-key-only; re-enroll
**rotates + revokes** (old key → 401, new key → 200); directive served only in recovery mode +
**expires**; clear disables re-enroll.
## v0.10.0 — slice 10B: signed-op job completion (clear-job) (2026-06-10)
The hub half of slice 10B is small by design — the hub stores + serves the operator-signed blobs