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:
@@ -33,9 +33,10 @@ type (
|
||||
Generate() (privPEM, pubAuthorized string, err error)
|
||||
}
|
||||
// KeyInstaller installs the pub line on the box using the one-time password, then verifies passwordless
|
||||
// key auth with the private key. Fails if the install or the verify fails.
|
||||
// key auth with the private key. It MUST pin the VERIFIED knownHosts line (from the scan) on the
|
||||
// connection — never blind-TOFU — so a MITM cannot swap the key between the scan and the install.
|
||||
KeyInstaller interface {
|
||||
Install(ctx context.Context, host, user string, port int, password, privPEM, pubAuthorized string) error
|
||||
Install(ctx context.Context, host, user string, port int, password, privPEM, pubAuthorized, knownHosts string) error
|
||||
}
|
||||
// OffboxEnabler configures the offbox target (key + known_hosts + target) and goes EscrowState="pending"
|
||||
// (the fork-4 enable path).
|
||||
@@ -132,8 +133,10 @@ func (b *Bridge) Reconcile(ctx context.Context) error {
|
||||
return fmt.Errorf("offsite-apply: consume one-time password: %w", err)
|
||||
}
|
||||
|
||||
// 4) Install the pubkey using the password (proven ssh-copy-id -s -f), verify key auth.
|
||||
if err := b.Installer.Install(ctx, o.Host, o.User, port, password, privPEM, pubAuthorized); err != nil {
|
||||
// 4) Install the pubkey using the password (proven ssh-copy-id -s -f), verify key auth. Pin the
|
||||
// scanner-VERIFIED known_hosts line on the install/verify connections — never accept-new — so a MITM
|
||||
// cannot substitute a different key in the gap between the scan and the install.
|
||||
if err := b.Installer.Install(ctx, o.Host, o.User, port, password, privPEM, pubAuthorized, knownHostsLine); err != nil {
|
||||
// The password is now SPENT but install failed — a loud, distinct signal: the operator must reset
|
||||
// the box password on the hub and let the bridge retry. Do NOT mark applied.
|
||||
b.logf("[ERROR] [offsite-apply] key install FAILED after consuming the one-time password for %s@%s — the password is spent; reset it on the hub to retry: %v", o.User, o.Host, err)
|
||||
|
||||
Reference in New Issue
Block a user