Files
felhom-agent/REPORT.md
T
admin fae11020a5 slice 7 escrow: agent contract test + CHANGELOG/REPORT/CLAUDE (v0.9.0)
Agent half of slice-7 close-out finalized: cross-repo wire-contract test
(escrowUploadRequest mirrors the hub), v0.9.0 CHANGELOG entry + REPORT + CLAUDE
version line. No secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 07:45:38 +02:00

3.5 KiB

REPORT — Slice 7 close-out: PBS recovery-code escrow creation (v0.9.0) (2026-06-10)

Overwrite-latest report (most recent significant work only). Cumulative history lives in CHANGELOG.md. Implements the agent half of TASK — Slice 7 close-out: PBS recovery-code escrow creation. The hub opaque-storage half is in felhom.eu (hub v0.8.0).

Outcome

The first code that touches the PBS client encryption key K and introduces the customer recovery code R. Default posture is zero-knowledge: Felhom holds an opaque R-wrapped blob it cannot open; the customer holds R. The wrap→recover→restore round-trip was proven on a fenced throwaway before any code (felhom.eu/documentation/tests/slice7-escrow-spike-findings.md) — the R-recovered key restored a real encrypted snapshot.

Secret discipline (the overriding rule — held throughout)

  • R: crypto/rand, ≥128 bits, surfaced exactly once (selftest stdout), never logged, persisted, committed, or in this report. The wrap pty's echoed passphrase is discarded (F-A2).
  • K: read by location, never modified (Wrap operates on a copy; the live key file is byte-unchanged — asserted live by sha256), never logged.
  • The blob is opaque ciphertext; the hub stores bytes and never decrypts.

What landed (internal/escrow)

  • Create → generate R (10 EFF-wordlist words ≈ 129 bits) → Wrap K under R via the PBS-native proxmox-backup-client key change-passphrase --kdf scrypt, driven over a stdlib pty (x/sys/unix; the command is TTY-only, spike F-A1) with output discarded (F-A2) → self-verify the blob unwraps back to K (fingerprint match) before shipping → return R SEPARATELY (so it can't be logged via the result) + the opaque blob.
  • Opt-in outputs: (b) R-wrapped offline copy (two-factor, no extra trust); (a) raw paperkey (single-factor, unrevocable — loud caveat).
  • --selftest=escrow-create (-storage/-paperkey/-offline/-upload): surfaces R once, prints the opaque blob's size/fingerprint/posture, and with -upload PUTs it to the hub (/api/v1/hosts/{host_id}/escrow, per-host key).
  • Config: escrow{posture, pbs_storage_id} (zero_knowledge default); PBSEncKeyPath (the <id>.enc).
  • Runtime dependency: the proxmox-backup-client CLI.

Tests

go test ./... green; -race green on the build server (pure-Go parts). R entropy ≥128 / 10-word format / 200-draw uniqueness; the agent→hub wire-contract key-set (mirrors the hub). The integration round-trip (wrap→unwrap fingerprint match, wrong-R fails, live K byte-unchanged, blob ≠ plaintext) is guarded to linux+proxmox-backup-client and exercised by the live selftest.

Live validation (demo-felhom)

--selftest=escrow-create -storage felhom-pbs: R (10 words) surfaced once to stdout; blob 383 B opaque (key fingerprint 01:36:e9…, posture zero_knowledge, ~129-bit R); self-verify confirmed recoverability; the live K (felhom-pbs.enc) sha256 byte-unchanged; the exact R was absent from stderr and the agent journal. (Upload to the hub validated after hub v0.8.0 deploy — see felhom.eu REPORT.)

Deferred (stated, not built)

Escrow consumption / restore-mode serving → slice 10 (host-loss DR; the spike pre-validated it). The Felhom-holds-a-key posture (the one dangerous topology cell) is documented (doc 03 §8a) and implemented only on an explicit customer opt-in. No secrets committed.