diff --git a/drivers/mysql/resources/spec.json b/drivers/mysql/resources/spec.json index 595b44bf3..f4fe19b12 100644 --- a/drivers/mysql/resources/spec.json +++ b/drivers/mysql/resources/spec.json @@ -210,6 +210,21 @@ "title": "SSH Passphrase", "description": "Passphrase to decrypt the encrypted SSH private key", "format": "password" + }, + "host_key_verification_mode": { + "type": "string", + "title": "Host Key Verification Mode", + "description": "Mode for SSH host key verification. Use 'strict' to verify against known_hosts file, 'insecure' to skip verification, or leave empty for insecure (default)", + "enum": [ + "strict", + "insecure" + ], + "default": "insecure" + }, + "known_hosts_file_path": { + "type": "string", + "title": "Known Hosts File Path", + "description": "Absolute path to the known_hosts file. Required when host_key_verification_mode is 'strict'" } }, "required": [ @@ -248,6 +263,21 @@ "description": "Password to authenticate with the SSH server", "format": "password", "minLength": 1 + }, + "host_key_verification_mode": { + "type": "string", + "title": "Host Key Verification Mode", + "description": "Mode for SSH host key verification. Use 'strict' to verify against known_hosts file, 'insecure' to skip verification, or leave empty for insecure (default)", + "enum": [ + "strict", + "insecure" + ], + "default": "insecure" + }, + "known_hosts_file_path": { + "type": "string", + "title": "Known Hosts File Path", + "description": "Absolute path to the known_hosts file. Required when host_key_verification_mode is 'strict'" } }, "required": [ diff --git a/drivers/postgres/resources/spec.json b/drivers/postgres/resources/spec.json index e13f774e7..97d031945 100644 --- a/drivers/postgres/resources/spec.json +++ b/drivers/postgres/resources/spec.json @@ -161,6 +161,21 @@ "title": "SSH Passphrase", "description": "Passphrase to decrypt the encrypted SSH private key", "format": "password" + }, + "host_key_verification_mode": { + "type": "string", + "title": "Host Key Verification Mode", + "description": "Mode for SSH host key verification. Use 'strict' to verify against known_hosts file, 'insecure' to skip verification, or leave empty for insecure (default)", + "enum": [ + "strict", + "insecure" + ], + "default": "insecure" + }, + "known_hosts_file_path": { + "type": "string", + "title": "Known Hosts File Path", + "description": "Absolute path to the known_hosts file. Required when host_key_verification_mode is 'strict'" } }, "required": [ @@ -199,6 +214,21 @@ "description": "Password to authenticate with the SSH server", "format": "password", "minLength": 1 + }, + "host_key_verification_mode": { + "type": "string", + "title": "Host Key Verification Mode", + "description": "Mode for SSH host key verification. Use 'strict' to verify against known_hosts file, 'insecure' to skip verification, or leave empty for insecure (default)", + "enum": [ + "strict", + "insecure" + ], + "default": "insecure" + }, + "known_hosts_file_path": { + "type": "string", + "title": "Known Hosts File Path", + "description": "Absolute path to the known_hosts file. Required when host_key_verification_mode is 'strict'" } }, "required": [ diff --git a/utils/spec/uischema.go b/utils/spec/uischema.go index a4c1d9ae5..b3708ff5b 100644 --- a/utils/spec/uischema.go +++ b/utils/spec/uischema.go @@ -95,6 +95,22 @@ const PostgresUISchema = `{ "ui:options": { "rows": 1 } + }, + "host_key_verification_mode": { + "ui:widget": "radio", + "ui:grid": [ + { "strict": 12, "insecure": 12} + ], + "ui:options": { + "title": false, + "description": false + } + }, + "known_hosts_file_path": { + "ui:widget": "text", + "ui:options": { + "placeholder": "Enter the path to the known_hosts file" + } } } }` @@ -156,6 +172,22 @@ const MySQLUISchema = `{ "ui:options": { "rows": 1 } + }, + "host_key_verification_mode": { + "ui:widget": "radio", + "ui:grid": [ + { "strict": 12, "insecure": 12} + ], + "ui:options": { + "title": false, + "description": false + } + }, + "known_hosts_file_path": { + "ui:widget": "text", + "ui:options": { + "placeholder": "Enter the path to the known_hosts file" + } } } }` diff --git a/utils/ssh.go b/utils/ssh.go index f7b1fa8a3..2c1af9228 100644 --- a/utils/ssh.go +++ b/utils/ssh.go @@ -8,17 +8,25 @@ import ( "time" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" ) type SSHConfig struct { - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - Username string `json:"username,omitempty"` - PrivateKey string `json:"private_key,omitempty"` - Passphrase string `json:"passphrase,omitempty"` - Password string `json:"password,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Username string `json:"username,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + Passphrase string `json:"passphrase,omitempty"` + Password string `json:"password,omitempty"` + HostKeyVerificationMode string `json:"host_key_verification_mode,omitempty"` + KnownHostsFilePath string `json:"known_hosts_file_path,omitempty"` } +const ( + StrictHostKeyVerification = "strict" + InsecureHostKeyVerification = "insecure" +) + func (c *SSHConfig) Validate() error { if c.Host == "" { return errors.New("ssh host is required") @@ -36,9 +44,48 @@ func (c *SSHConfig) Validate() error { return errors.New("private key or password is required") } + if c.HostKeyVerificationMode == StrictHostKeyVerification { + if c.KnownHostsFilePath == "" { + return errors.New("known_hosts file path is required for strict verification") + } + } + + if c.HostKeyVerificationMode == "" { + c.HostKeyVerificationMode = InsecureHostKeyVerification + } + return nil } +func (c *SSHConfig) getHostKeyCallback() (ssh.HostKeyCallback, error) { + strictStrategy := func() (ssh.HostKeyCallback, error) { + // need an absolute path to the known_hosts file + if err := CheckIfFilesExists(c.KnownHostsFilePath); err != nil { + return nil, fmt.Errorf("known_hosts file validation failed: %w", err) + } + + callback, err := knownhosts.New(c.KnownHostsFilePath) + if err != nil { + return nil, fmt.Errorf("failed to load known_hosts file: %w", err) + } + + return callback, nil + } + + insecureStrategy := func() (ssh.HostKeyCallback, error) { + return ssh.InsecureIgnoreHostKey(), nil // #nosec G106 + } + + switch c.HostKeyVerificationMode { + case InsecureHostKeyVerification: + return insecureStrategy() + case StrictHostKeyVerification: + return strictStrategy() + default: + return nil, fmt.Errorf("unknown host key verification strategy: %s", c.HostKeyVerificationMode) + } +} + func (c *SSHConfig) SetupSSHConnection() (*ssh.Client, error) { err := c.Validate() if err != nil { @@ -58,12 +105,15 @@ func (c *SSHConfig) SetupSSHConnection() (*ssh.Client, error) { authMethods = append(authMethods, ssh.PublicKeys(signer)) } + hostKeyCallback, err := c.getHostKeyCallback() + if err != nil { + return nil, fmt.Errorf("failed to get host key callback: %s", err) + } + sshCfg := &ssh.ClientConfig{ - User: c.Username, - Auth: authMethods, - // Allows everyone to connect to the server without verifying the host key - // TODO: Add proper host key verification - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // #nosec G106 + User: c.Username, + Auth: authMethods, + HostKeyCallback: hostKeyCallback, Timeout: 30 * time.Second, }