Files
felhom-agent/REPORT.md
T
admin 72d7f05bf1 docs: REPORT + CONTEXT for S5 safe halves (WG-key install + directive→plan)
Parts 1-2 shipped + deployed (v0.69.0); Part 3 not needed (sqlite3 export);
Part 4-A re-attach safety already unit-proven; Part 4-B destructive 9201 drill
prepared + operator-gated (pre-flight green), not executed by CC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 21:12:19 +02:00

4.9 KiB

REPORT — S5: host-loss DR (recovered WG-key install + directive→PLAN; safe halves shipped)

Date: 2026-07-04 · Class: implementation (safe halves) + diagnosis + a prepared, operator-present destructive drill (NOT executed by CC). Design of record: doc-06 §3.5 + S5 row; doc-01 §9.

1. Baselines & commits (felhom-agent → main)

Start: 567cf9f (v0.68.0). bd4bced v0.69.0 — the two safe DR halves (Parts 1-2). This commit — REPORT + CONTEXT + doc-06 S5 row.

2. Part 1 (SAFE, shipped) — install the recovered WG key

wgtunnel.InstallRecoveredKey(stateDir, privB64) writes an escrow-recovered WG private key (32-byte base64, re-encoded canonical) to the key file so the tunnel re-establishes with the same identity/pubkey → same hub /32, no fresh keygen. CREATE-ONLY — refuses if a key file exists (a present key may be a live identity); the value is never logged. Wired into --selftest=identity-consume -install-wg-key (opt-in; installs bundle.WGPrivateKey after UnwrapIdentityBundle; a pre-S3 blob with no WG key → logged fallback to fresh keygen + re-register, which keeps the /32 via hub re-key-in-place). Test + red-proof: install→EnsureKey LOADS it (same pubkey, no keygen); a second install REFUSES (create-only — red-proofed against dropping the exists-check); invalid key → error, nothing written.

3. Part 2 (SAFE, shipped) — consume restore_directive → PLAN

New internal/dr: the host_loss restore_directive (was logged-and-ignored in syncer.go) is consumed via the desired.Syncer.AddConsumer raw seam into an inspectable RestorePlan — per guest {vmid, archive, target storage, sizing}, per drive {durable_id → expected mount}, + the offsite PBS coord. Derive-and-surface only: the Consumer holds NO restore/destroy dependency, so "execute nothing" is structural. guest_loss/absent → no plan; recipe fetched on-demand (rare directive) via a fresh Collect. Test + red-proof: host_loss builds the expected plan; guest_loss/absent/nil-recipe → none (red-proofed against a relaxed mode gate); the consumer surfaces on host_loss + clears otherwise + consults the recipe only then. Full go test ./... green; deployed 0.69.0; -install-wg-key flag live; daemon starts clean (dr consumer wired).

4. Part 3 — hub escrow export: NOT needed

The operator has a clean blob-export path already: sqlite3 … "SELECT writefile('/tmp/blob', blob), writefile('/tmp/idblob', identity_blob) FROM host_escrow WHERE host_id='demo-felhom-01'" on a kubectl cp'd hub.db. No hub change (respects the read-only-unless-no-clean-way gate).

5. Part 4-A — re-attach wrong-disk safety: ALREADY PROVEN (no new probe built)

The re-attach matcher is storage.ResolveStorageDevice (the uuid:<fs-uuid> scheme DR drives use). Its wrong-disk guard is unit-established: TestResolveStorageDevice_ToleratesDeviceLetterMove (a MATCHING UUID resolves to the current /dev path) + TestResolveStorageDevice_AbsentAndScheme (an absent/mismatched UUID ERRORS — "skips a gone drive instead of fail-mounting"; a bare node / non-uuid: scheme is refused). It resolves ONLY the exact UUID's symlink — never a "nearest" disk. Combined with S4.1's restore-to-scratch proof, the composition's safety is established; a new live scratch+marker-drive probe would add confidence but not new safety, so it was not built (recorded).

6. Part 4-B — the destructive in-place 9201 drill: PREPARED + OPERATOR-GATED (not executed)

Pre-flight GREEN (non-destructive): offsite ct/9201 snapshots present (the restore source); 9201 running; the escrow blob is on the hub (383 B + 499 B identity, offsite key b0:fe:2a…), exportable via sqlite3 writefile; S4.1 already proved 9201 restores from offsite (pass:true). The destructive drill (destroy 9201 → restore in place from offsite → re-attach the data drive by durable_id) is operator-present + STOP-gated: the operator runs the R-consume steps (escrow-consume + identity-consume -install-wg-key, R via env — CC never runs these, §9-4a) and confirms the destroy at the STOP checkpoint. Prepared procedure + the go/no-go is with the operator (destroying the live demo controller is their decision, even with the backup confirmed recoverable).

7. NOT yet live-validated / observations

  • The live consume→install→tunnel→restore pipeline (the Part 4-B drill) — operator-present, pending the operator's go-ahead + R-run. Parts 1-2 are unit-proven + deployed; their live consume path is exercised only in that drill (R-gated).
  • Deferred (per slice scope): guest_loss DR; hub-driven full-auto DR (the hub issuing recovery-mode + directive itself); multi-guest host-loss. true-CGNAT-SIM retest still parked.
  • Observation: the DR plan is surfaced via structured log + Consumer.LastPlan(); a report field (so the hub/operator sees the plan without the box's journal) is a small future refinement.