ssh: trust-on-first-use host key pinning everywhere
Guest host-key verification was off in all three SSH paths:
* Go SSH (internal/guest/ssh.go) used ssh.InsecureIgnoreHostKey
* `banger vm ssh` passed StrictHostKeyChecking=no
+ UserKnownHostsFile=/dev/null
* `~/.ssh/config` Host *.vm shipped the same posture into the
user's global config
Now each path verifies against a banger-owned known_hosts file at
`~/.local/state/banger/ssh/known_hosts` with TOFU semantics:
* First dial to a VM pins the key.
* Subsequent dials require an exact match. A mismatch fails with
an explicit "possible MITM" error.
* `vm delete` removes the entries so a future VM reusing the IP
or name re-pins cleanly.
* The user's `~/.ssh/known_hosts` is untouched.
Changes:
internal/guest/known_hosts.go (new) — OpenSSH-compatible parser,
TOFUHostKeyCallback, RemoveKnownHosts. Process-wide mutex
around the file.
internal/guest/ssh.go — Dial and WaitForSSH grew a knownHostsPath
parameter threaded through the callback. Empty path keeps the
insecure callback (tests + throwaway tools only; documented).
internal/daemon/{guest_sessions,session_attach,session_lifecycle,
session_stream}.go — call sites pass d.layout.KnownHostsPath.
internal/daemon/ssh_client_config.go — the ~/.ssh/config Host *.vm
block now points at banger's known_hosts and uses
StrictHostKeyChecking=accept-new. Missing path → fail closed.
internal/daemon/vm_lifecycle.go — deleteVMLocked drops known_hosts
entries for the VM's IP and DNS name via removeVMKnownHosts.
internal/cli/banger.go — sshCommandArgs swaps StrictHostKeyChecking
no + /dev/null for banger's file + accept-new. Path resolution
failure falls through to StrictHostKeyChecking=yes.
internal/paths/paths.go — Layout gains SSHDir + KnownHostsPath;
Ensure creates SSHDir at 0700.
Tests (internal/guest/known_hosts_test.go): pin on first use, accept
matching key on second dial, reject mismatch, empty path skips
checking, RemoveKnownHosts drops the entry, re-pin works after
remove. Existing daemon + cli tests updated to assert the new
posture and regression-guard against the old flags.
Live verified: vm run writes the pin to banger's known_hosts at 0600
inside a 0700 dir; banger vm ssh + ssh root@<vm>.vm both succeed
using the pin; vm delete clears it.
This commit is contained in:
parent
a59958d4f5
commit
ae14b9499d
14 changed files with 634 additions and 47 deletions
|
|
@ -13,8 +13,10 @@ func TestSyncVMSSHClientConfigCreatesManagedBlock(t *testing.T) {
|
|||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
knownHostsPath := filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts")
|
||||
layout := paths.Layout{
|
||||
ConfigDir: filepath.Join(homeDir, ".config", "banger"),
|
||||
ConfigDir: filepath.Join(homeDir, ".config", "banger"),
|
||||
KnownHostsPath: knownHostsPath,
|
||||
}
|
||||
keyPath := filepath.Join(layout.ConfigDir, "ssh", "id_ed25519")
|
||||
|
||||
|
|
@ -38,12 +40,23 @@ func TestSyncVMSSHClientConfigCreatesManagedBlock(t *testing.T) {
|
|||
"IdentitiesOnly yes",
|
||||
"BatchMode yes",
|
||||
"PasswordAuthentication no",
|
||||
"UserKnownHostsFile /dev/null",
|
||||
"UserKnownHostsFile " + knownHostsPath,
|
||||
"StrictHostKeyChecking accept-new",
|
||||
} {
|
||||
if !strings.Contains(userContent, want) {
|
||||
t.Fatalf("user config = %q, want %q", userContent, want)
|
||||
}
|
||||
}
|
||||
// Regression: the legacy posture (StrictHostKeyChecking no +
|
||||
// UserKnownHostsFile /dev/null) must never reappear.
|
||||
for _, must := range []string{
|
||||
"StrictHostKeyChecking no",
|
||||
"UserKnownHostsFile /dev/null",
|
||||
} {
|
||||
if strings.Contains(userContent, must) {
|
||||
t.Fatalf("user config leaked legacy posture %q:\n%s", must, userContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncVMSSHClientConfigReplacesManagedIncludeBlock(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue