# REPORT — slice 10D (agent half): DR capstone — identity escrow + restore-mode consumption (v0.18.0) (2026-06-10) > Overwrite-latest report. Cumulative history: [CHANGELOG.md](CHANGELOG.md). ## What was implemented The agent half of the slice-10 DR capstone (closes slice 10), grounded by both 10-series spikes (escrow-consumption + identity-restore). The hub half (recovery-mode, re-enroll, directive serving) is hub v0.11.0. **Rotation model (locked):** the hub holds no Cloudflare write-power; the destructive tunnel/PBS rotation is the operator's step from a trusted environment (same spirit as 10B). ### Identity escrow (`internal/escrow/identity.go`) - `WrapIdentity`/`UnwrapIdentity` (+ `…Bundle`) wrap the `{tunnel_token, pbs_token}` bundle under the SAME recovery code `R` via **`age`** (scrypt + ChaCha20-Poly1305 — vetted passphrase-AEAD, not hand-rolled), **reusing the K-escrow pty mechanism** (passphrase via the tty, data via files; `R`/tokens never logged). Same two-factor, zero-knowledge shape as the K-escrow; a **wrong R fails closed** (no bundle). `age` is a runtime dep for the identity path (like proxmox-backup-client for K). - `escrow.Create` gains an optional `IdentityBundle` → emits `IdentityBlob` under the same R (additive; the K-escrow + 10C `Consume` are byte-unchanged; self-verifies the identity round-trip). - `--selftest=escrow-create -identity-bundle -directive ` (wrap + upload the identity blob + non-secret directive) and `--selftest=identity-consume -blob -keydest ` (R via `FELHOM_RECOVERY_CODE`; tokens written 0600, never logged) — the real code, drillable. ### Restore-mode orchestration (10D.3) Composed from proven pieces: re-enroll (hub) → fetch directive (10A) → `Consume` K (10C, fingerprint -gated) + `identity-consume` (R by hand) → restore guests from PBS (restore-overwrite gated by 10B) → re-establish the tunnel (recovered token + reconstituted origin — proven in the 10D spike). The destructive cred rotation is the operator's step. ## Tests - identity round-trips (wrap→unwrap byte-identical; blob is opaque ciphertext); wrong R fails closed + blob stays retryable; input validation. K-escrow/10C unchanged. `go test ./...` green (age integration tests gated to a host with the `age` CLI). ## Versioning / docs - Version `0.17.0 → 0.18.0`; `CHANGELOG.md`. Doc 03 §9 (10D done → **slice 10 CLOSED**) + the host-loss DR flow with the operator-side rotation model. ## Deferred (non-blocking, per the locked model) - The hub Config DR/Recovery **web UI** (functional via the recovery-mode admin API today) + a small operator rotation CLI (the tunnel/PBS rotation + stale-connector delete is a documented operator procedure from the trusted environment — the hub never holds Cloudflare write-power). ## Pending - The operator-in-the-loop **DR drill** (live, throwaway identity): host-loss → recovery mode → re-enroll (old key revoked) → R → consume → restore → re-establish → operator rotates/retires the old connector → only the new box serves. CC drives the `[CC]` steps; the operator does the trusted steps.