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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user