From e8111a419f061db114f78a3fb0f05273b6591e5e Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Tue, 14 Apr 2026 10:00:59 +0200 Subject: [PATCH] feat: auto-accept only new SSH host keys using "-o StrictHostKeyChecking: accept-new" (#303) * feat: add "StrictHostKeyChecking no" to ssh Disable host key checking to prevent interaction with ssh. Fixes: #297 Signed-off-by: Miek Gieben * use accept-new instead of no Signed-off-by: Miek Gieben * fix tests Signed-off-by: Miek Gieben * Fix tests here as well Signed-off-by: Miek Gieben --------- Signed-off-by: Miek Gieben --- internal/sshexec/sshcli.go | 1 + internal/sshexec/sshcli_test.go | 16 ++++++++-------- pkg/client/connector/sshcli.go | 2 ++ pkg/client/connector/sshcli_test.go | 14 +++++++------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/internal/sshexec/sshcli.go b/internal/sshexec/sshcli.go index 1f4413b5..70a5ac12 100644 --- a/internal/sshexec/sshcli.go +++ b/internal/sshexec/sshcli.go @@ -33,6 +33,7 @@ func NewSSHCLIRemote(user, host string, port int, keyPath string) *SSHCLIRemote func (r *SSHCLIRemote) newSSHCommand(ctx context.Context, cmd string) *exec.Cmd { args := []string{ "-o", "ConnectTimeout=5", + "-o", "StrictHostKeyChecking=accept-new", // Disable pseudo-terminal allocation to prevent SSH from executing as a login shell. "-T", } diff --git a/internal/sshexec/sshcli_test.go b/internal/sshexec/sshcli_test.go index de535a8e..bf0601a0 100644 --- a/internal/sshexec/sshcli_test.go +++ b/internal/sshexec/sshcli_test.go @@ -24,7 +24,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { host: "example.com", cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "example.com", "whoami", }, }, { @@ -33,7 +33,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { host: "example.com", cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "root@example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "root@example.com", "whoami", }, }, { @@ -42,7 +42,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { port: 2222, cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "2222", "example.com", "whoami", }, }, { @@ -51,7 +51,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { keyPath: "/path/to/key", cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-i", "/path/to/key", "example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-i", "/path/to/key", "example.com", "whoami", }, }, { @@ -61,7 +61,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { port: 2222, cmd: "ls -la", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "ubuntu@192.168.1.10", "ls -la", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "2222", "ubuntu@192.168.1.10", "ls -la", }, }, { @@ -71,7 +71,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { keyPath: "/path/to/key", cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-i", "/path/to/key", "root@example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-i", "/path/to/key", "root@example.com", "whoami", }, }, { @@ -81,7 +81,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { keyPath: "~/.ssh/id_rsa", cmd: "whoami", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-p", "22", "-i", "~/.ssh/id_rsa", "example.com", "whoami", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "22", "-i", "~/.ssh/id_rsa", "example.com", "whoami", }, }, { @@ -92,7 +92,7 @@ func TestSSHCLIRemote_newSSHCommand(t *testing.T) { keyPath: "~/.ssh/id_rsa", cmd: "sudo bash -c 'echo hello'", expected: []string{ - "ssh", "-o", "ConnectTimeout=5", "-T", "-p", "2222", "-i", "~/.ssh/id_rsa", + "ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "2222", "-i", "~/.ssh/id_rsa", "admin@server.local", "sudo bash -c 'echo hello'", }, }, diff --git a/pkg/client/connector/sshcli.go b/pkg/client/connector/sshcli.go index 5d65c9bd..bc1fd73d 100644 --- a/pkg/client/connector/sshcli.go +++ b/pkg/client/connector/sshcli.go @@ -131,6 +131,8 @@ func (c *SSHCLIConnector) buildSSHArgs() []string { // Disable interactive prompts (e.g., passphrase input) to prevent interference with the TUI. // Authentication must succeed non-interactively via SSH agent or unencrypted key. args = append(args, "-o", "BatchMode=yes") + // Disable host key checking for parity with go+ssh. + args = append(args, "-o", "StrictHostKeyChecking=accept-new") // Disable pseudo-terminal allocation to prevent SSH from executing as a login shell. args = append(args, "-T") diff --git a/pkg/client/connector/sshcli_test.go b/pkg/client/connector/sshcli_test.go index d6062125..0445dff0 100644 --- a/pkg/client/connector/sshcli_test.go +++ b/pkg/client/connector/sshcli_test.go @@ -23,7 +23,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { Host: "example.com", }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "root@example.com"}, }, { name: "basic connection without control socket", @@ -32,7 +32,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { Host: "example.com", }, controlSockPath: "", - expected: []string{"-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "root@example.com"}, + expected: []string{"-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "root@example.com"}, }, { name: "with custom port", @@ -42,7 +42,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { Port: 2222, }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "-p", "2222", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "2222", "root@example.com"}, }, { name: "with identity file", @@ -52,7 +52,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { KeyPath: "/path/to/key", }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "-i", "/path/to/key", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "-i", "/path/to/key", "root@example.com"}, }, { name: "all options combined", @@ -64,7 +64,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { SockPath: "/custom/path/uncloud.sock", }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "-p", "2222", "-i", "/path/to/key", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "2222", "-i", "/path/to/key", "root@example.com"}, }, { name: "port 0 not included", @@ -74,7 +74,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { Port: 0, }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "root@example.com"}, }, { name: "port 22 included when explicit", @@ -84,7 +84,7 @@ func TestSSHCLIConnector_buildSSHArgs(t *testing.T) { Port: 22, }, controlSockPath: "/tmp/test.sock", - expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-T", "-p", "22", "root@example.com"}, + expected: []string{"-o", "ControlMaster=auto", "-o", "ControlPath=/tmp/test.sock", "-o", "ControlPersist=10m", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-T", "-p", "22", "root@example.com"}, }, }