diff options
| author | Shulhan <ms@kilabit.info> | 2023-08-29 00:12:02 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-08-29 00:25:24 +0700 |
| commit | e9de137bd7dd0e8396172d4152fafe168b4607cc (patch) | |
| tree | f5c0ff85b16779adaccb016a27f83ffe0dfed739 /lib/ssh/client.go | |
| parent | 7be4871727ca1e6f48bf8f57a2fdab09b6a2b7f1 (diff) | |
| download | pakakeh.go-e9de137bd7dd0e8396172d4152fafe168b4607cc.tar.xz | |
lib/ssh: use UserKnownHostFile from configuration in NewClientInteractive
Previously, the ssh Client always use InsecureIgnoreHostKey in
HostKeyCallback.
This may post security issue, like man-in-the-middle attack, since we
did not check the server host key with one of key that known by client
from UserKnownHostFile (for example ~/.ssh/known_hosts).
This changes use the SSH section UserKnownHostFile from configuration
(default to ~/.ssh/known_hosts) to check if the server host key is
valid.
The NewClientInteractive will return an error, "key is unknown", if host
key not exist in UserKnownHostFile or "key is mismatch" if host key
not match with one registered in UserKnownHostFile.
This changes depends on patch of golang.org/x/crypto [1] that has not
reviewed yet, so we need to replace it with one that contains the patch.
[1] https://go-review.googlesource.com/c/crypto/+/523555
Diffstat (limited to 'lib/ssh/client.go')
| -rw-r--r-- | lib/ssh/client.go | 106 |
1 files changed, 98 insertions, 8 deletions
diff --git a/lib/ssh/client.go b/lib/ssh/client.go index a6841e5f..a1593303 100644 --- a/lib/ssh/client.go +++ b/lib/ssh/client.go @@ -5,6 +5,7 @@ package ssh import ( + "errors" "fmt" "io" "log" @@ -14,6 +15,7 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/knownhosts" libos "github.com/shuLhan/share/lib/os" "github.com/shuLhan/share/lib/ssh/config" @@ -26,11 +28,15 @@ type Client struct { *ssh.Client config *ssh.ClientConfig + configHostKeyCallback ssh.HostKeyCallback + cfg *config.Section stdout io.Writer stderr io.Writer remoteAddr string + + listKnownHosts []string } // NewClientInteractive create a new SSH connection using predefined @@ -42,6 +48,15 @@ type Client struct { // // If the IdentityFile is encrypted, it will prompt for passphrase in // terminal. +// +// The following section keys are recognized and implemented by Client, +// - Hostname +// - IdentityAgent +// - IdentityFile +// - Port +// - User +// - UserKnownHostsFile, setting this to "none" will set HostKeyCallback +// to [ssh.InsecureIgnoreHostKey]. func NewClientInteractive(cfg *config.Section) (cl *Client, err error) { if cfg == nil { return nil, nil @@ -58,8 +73,7 @@ func NewClientInteractive(cfg *config.Section) (cl *Client, err error) { cl = &Client{ sysEnvs: libos.Environments(), config: &ssh.ClientConfig{ - User: cfg.User(), - HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: cfg.User(), }, cfg: cfg, stdout: os.Stdout, @@ -67,6 +81,11 @@ func NewClientInteractive(cfg *config.Section) (cl *Client, err error) { remoteAddr: fmt.Sprintf(`%s:%s`, cfg.Hostname(), cfg.Port()), } + err = cl.setConfigHostKeyCallback() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + var sshAgentSockPath = cfg.IdentityAgent() if len(sshAgentSockPath) > 0 { var sshAgentSock net.Conn @@ -83,11 +102,19 @@ func NewClientInteractive(cfg *config.Section) (cl *Client, err error) { return nil, fmt.Errorf(`%s: %w`, logp, err) } - signer = cl.dialWithSigners(signers) + signer, err = cl.dialWithSigners(signers) if signer != nil { // Client connected with one of the key in agent. return cl, nil } + + var errKey *knownhosts.KeyError + if errors.As(err, &errKey) { + // Host key is either unknown or mismatch with one + // of known_hosts files, so no need to continue with + // dialWithPrivateKeys. + return nil, fmt.Errorf(`%s: %w`, logp, err) + } } if len(cfg.IdentityFile) == 0 { @@ -102,24 +129,83 @@ func NewClientInteractive(cfg *config.Section) (cl *Client, err error) { return cl, nil } +// setConfigHostKeyCallback set the config.HostKeyCallback based on the +// UserKnownHostsFile in the Section. +// If one of the UserKnownHostsFile set to "none" it will use +// [ssh.InsecureIgnoreHostKey]. +func (cl *Client) setConfigHostKeyCallback() (err error) { + var ( + logp = `setConfigHostKeyCallback` + userKnownHosts = cl.cfg.UserKnownHostsFile() + + knownHosts string + ) + + for _, knownHosts = range userKnownHosts { + if knownHosts == config.ValueNone { + // If one of the UserKnownHosts set to "none" always + // accept the remote hosts. + cl.config.HostKeyCallback = ssh.InsecureIgnoreHostKey() + return nil + } + + knownHosts, err = libos.PathUnfold(knownHosts) + if err != nil { + return fmt.Errorf(`%s: %s: %w`, logp, knownHosts, err) + } + + _, err = os.Stat(knownHosts) + if err == nil { + // Add the user known hosts file only if its exist. + cl.listKnownHosts = append(cl.listKnownHosts, knownHosts) + } + } + + cl.config.HostKeyCallback, err = knownhosts.New(cl.listKnownHosts...) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + + return nil +} + +// dialError return the error with clear information when the host key is +// missing or mismatch from known_hosts files. +func (cl *Client) dialError(logp string, errDial error) (err error) { + var ( + errKey *knownhosts.KeyError + ) + if errors.As(errDial, &errKey) { + if len(errKey.Want) == 0 { + err = fmt.Errorf(`%s: %w: server host key is missing from %+v`, logp, errDial, cl.listKnownHosts) + } else { + err = fmt.Errorf(`%s: %w: server host key mismatch in %+v`, logp, errDial, cl.listKnownHosts) + } + } else { + err = fmt.Errorf(`%s: %w`, logp, errDial) + } + return err +} + // dialWithSigners connect to the remote machine using AuthMethod PublicKeys // using each of signer in the list. // On success it will return the signer that can connect to remote address. -func (cl *Client) dialWithSigners(signers []ssh.Signer) (signer ssh.Signer) { +func (cl *Client) dialWithSigners(signers []ssh.Signer) (signer ssh.Signer, err error) { if len(signers) == 0 { - return nil + return nil, nil } - var err error + var logp = `dialWithSigners` for _, signer = range signers { cl.config.Auth = []ssh.AuthMethod{ ssh.PublicKeys(signer), } cl.Client, err = ssh.Dial(`tcp`, cl.remoteAddr, cl.config) if err == nil { - return signer + return signer, nil } + err = cl.dialError(logp, err) } - return nil + return nil, err } // dialWithPrivateKeys connect to the remote machine using each of the @@ -159,6 +245,10 @@ func (cl *Client) dialWithPrivateKeys(sshAgent agent.ExtendedAgent) (err error) if err == nil { break } + err = cl.dialError(logp, err) + } + if err != nil { + return err } if cl.Client == nil { // None of the private key can connect to remote address. |
