# Phase 2 Verification — v0.8 Coverage & Trust Hardening **Phase**: P02 — SSH trust hardening **Milestone**: v0.8 **REQs**: REQ-058, REQ-059 (+ latent TOFU bugfix closure) **Date**: 2026-08-04 **Result**: ✅ PASS (all 4 layers) ## Layer 1 — Structural ✅ - `go build ./...` PASS - `go vet ./...` PASS - No TODOs/stubs in new production code - All new exports resolve: `security.SSHFingerprintSHA256`, `security.WriteAtomic`, `proxmox.TOFUHostKeyCallback`, `proxmox.ResetHostKey`, `proxmox.pinnedHostKeyCallback`, `proxmox.Options.HostKeyFingerprint`, `cli.nodeKeyResetCmd` - Backward compatible: existing `BootstrapProxmox` callers work (the TOFU fix changed failure→success on first connect, which is the bugfix) ## Layer 2 — Behavioral ✅ - `go test ./internal/proxmox/... ./internal/cli/... ./internal/doctor/... ./internal/security/...` PASS - `go test -race ./internal/proxmox/... ./internal/doctor/...` PASS - Coverage held post-P02: proxmox 86.5% (was 87.1% in P01 — marginal change from new code paths), cli 76.7% (was 76.2%), doctor 70.4% (unchanged) - T02.10: all 7 end-to-end integration cases PASS (pinned correct/wrong, TOFU first/second/mismatch, key-reset+re-pin, pre-populated migration path) - T02.11: `--host-key-fingerprint` non-proxmox validation PASS ## Layer 3 — Security ✅ - **REQ-058**: `--host-key-fingerprint` fails closed on mismatch (pinnedHostKeyCallback returns error on any mismatch; bootstrap aborts before any SSH session command runs). SHA256: prefix validated up front. No downgrade to TOFU when pin supplied. - **REQ-059**: `orca node key-reset` is local-only (D-046) — only rewrites `~/.orca/known_hosts` via `security.WriteAtomic` (atomic temp+rename, AD-029); does NOT touch remote authorized_keys. Audit-logs `node.key_reset` with actor+node+host. - **TOFU bugfix (T02.6, v0.6 ship-defect)**: first-connect now captures + writes the key (was silently failing). Mismatch detection preserved (MITM protection). The `TOFUHostKeyCallback` is shared between bootstrap (T02.6) and doctor (T02.9) — GRILL condition #2 parity satisfied. - STRIDE: no new spoofing surface (pin is operator-supplied, fail-closed); no tampering (atomic rewrite); no repudiation (audit log); no info disclosure (fingerprint is a hash, not the key); no DoS (no network change); no elevation (local file ops only). - No secrets in test code (fake SSH keys generated in-test). ## Layer 4 — Quality ✅ - Tests follow existing conventions (table-driven, `fakeSSHServer` fixture reused, `sshDialer`/`sessionRunner` seams injected) - `TOFUHostKeyCallback` extracted to a shared helper (no duplication between bootstrap + doctor) — clean coupling (proxmox doesn't import doctor) - P0 issues: none. P1+ issues: none flagged. ## Requirement Coverage | REQ | Status | Evidence | |-----|--------|----------| | REQ-058 | ✅ Complete | `--host-key-fingerprint` flag (T02.3) + `pinnedHostKeyCallback` (T02.5) + `Result.HostKeyFingerprint` (T02.7) + e2e tests (T02.10) + validation (T02.11) | | REQ-059 | ✅ Complete | `orca node key-reset ` (T02.8) + `proxmox.ResetHostKey` atomic rewrite + audit log + e2e test (T02.10 case 6) | | (TOFU bugfix) | ✅ Complete | T02.6 fixes v0.6 ship-defect (first-connect `knownhosts.New` KeyError{Want:[]} treated as dial failure); T02.9 doctor parity | ## GRILL Conditions Check - **#1 (T02.6 labeled v0.6 ship-defect)**: ✅ commit `8b0cbe1` summary "TOFU capture bug — v0.6 ship-defect first-connect join always failed" - **#2 (T02.9 doctor parity)**: ✅ both bootstrap (`8b0cbe1`) and doctor (`2dcb143`) use the shared `proxmox.TOFUHostKeyCallback` wrapper ## Lessons - The v0.6 TOFU bug was a latent ship-defect: `knownhosts.New` returns `KeyError{Want:[]}` on first connect without writing, and the original code treated this as a dial failure. This means first-connect Proxmox join has been broken since v0.6 shipped — a strong argument for P01's coverage uplift (the 5.1% proxmox coverage hid this). v0.8 P03's `verify-reqs` would not have caught this (it's code-vs-doc drift, not doc-vs-doc) — P04 audit is the backstop. - Extracting `TOFUHostKeyCallback` to a shared helper was the right call for GRILL condition #2 — duplicating the wrapper in doctor would have created drift risk.