harden offsite apply-bridge: pin verified host key on the install/verify sessions (no TOFU)
The SSHCopyIDInstaller used StrictHostKeyChecking=accept-new on the ssh-copy-id and sftp-verify connections, so even though the bridge verifies the box host-key fingerprint against the hub descriptor BEFORE installing, the actual install connection was not pinned to that verified key — a MITM could substitute a different key in the gap between the scan and the install (TOCTOU). Now the bridge threads the scanner-verified known_hosts line into KeyInstaller, which writes it to a temp known_hosts and connects with StrictHostKeyChecking=yes + UserKnownHostsFile — the install/verify sessions refuse any key but the one the bridge already matched. Empty known_hosts now refuses to install. Test asserts the installer receives the pinned known_hosts; red-proofed by passing an empty line (the pre-fix TOFU shape) → test fails. Addresses the security-review "host-key TOFU after verify" finding on internal/offsiteapply/seams.go. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -8,7 +8,9 @@ On startup the controller reconciles the hub-served `offsite:` descriptor into a
|
||||
target — the controller half of hub-driven offsite provisioning.
|
||||
- `internal/config.OffsiteConfig` — the `offsite:` section (mirrors the hub descriptor incl. `host_fingerprint`).
|
||||
- `internal/offsiteapply.Bridge.Reconcile` — **verify-pin (no blind TOFU)** → generate keypair → **consume
|
||||
the one-time password** (single-use, never logged) → install pubkey (`sshpass -e ssh-copy-id -p 23 -s -f`)
|
||||
the one-time password** (single-use, never logged) → install pubkey (`sshpass -e ssh-copy-id -p 23 -s -f`,
|
||||
**pinning the scanner-verified `known_hosts` via `StrictHostKeyChecking=yes` — the install/verify sessions
|
||||
refuse any key but the one the bridge already matched, closing the scan→install MITM gap**)
|
||||
+ verify → configure offbox → `EscrowState="pending"` → persist the descriptor-hash marker LAST.
|
||||
**Idempotent** (marker prevents re-consuming a spent password) and **fail-safe** (any step fails → nothing
|
||||
persisted, retried next cycle; consumed-but-failed install logs a loud "password is spent — reset on the hub").
|
||||
@@ -21,8 +23,11 @@ target — the controller half of hub-driven offsite provisioning.
|
||||
|
||||
## Tests + companion red-proofs
|
||||
Green gate `go build ./... && go vet ./... && go test ./...` — **ALL-GREEN** (both repos).
|
||||
- `TestBridge_AppliesEndToEnd` — consume→verify-pin→install→configure→marker; asserts the enabler got the
|
||||
pinned known_hosts + the private key, and **the one-time password never appears in a log line**.
|
||||
- `TestBridge_AppliesEndToEnd` — consume→verify-pin→install→configure→marker; asserts **the installer AND
|
||||
the enabler both receive the scanner-verified known_hosts** + the private key, and **the one-time password
|
||||
never appears in a log line**. **Red-proof run:** passed an empty known_hosts to the installer (the pre-fix
|
||||
TOFU shape) → test FAILED ("installer must receive the scanner-verified known_hosts to pin (no TOFU), got \"\""). Reverted.
|
||||
Pinning-the-install-connection (not just the scan) is load-bearing — addresses the security-review TOFU-after-verify finding.
|
||||
- `TestBridge_HostKeyMismatchRefuses` — a scanned FP ≠ descriptor FP → refuse (no consume/install/configure/marker).
|
||||
**Red-proof run:** dropped the verify (`if false`) → the mismatch proceeded to install a wrong key → test
|
||||
FAILED ("mismatch must refuse, got <nil>"). Reverted. No-TOFU is load-bearing.
|
||||
|
||||
Reference in New Issue
Block a user