go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/ssh.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package connection
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"io"
    11  	"net"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	awsconf "github.com/aws/aws-sdk-go-v2/config"
    19  	"github.com/kevinburke/ssh_config"
    20  	"github.com/mitchellh/go-homedir"
    21  	rawsftp "github.com/pkg/sftp"
    22  	"github.com/rs/zerolog/log"
    23  	"github.com/spf13/afero"
    24  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    25  	"go.mondoo.com/cnquery/providers-sdk/v1/vault"
    26  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    27  	"go.mondoo.com/cnquery/providers/os/connection/ssh/awsinstanceconnect"
    28  	"go.mondoo.com/cnquery/providers/os/connection/ssh/awsssmsession"
    29  	"go.mondoo.com/cnquery/providers/os/connection/ssh/cat"
    30  	"go.mondoo.com/cnquery/providers/os/connection/ssh/scp"
    31  	"go.mondoo.com/cnquery/providers/os/connection/ssh/sftp"
    32  	"go.mondoo.com/cnquery/providers/os/connection/ssh/signers"
    33  	"go.mondoo.com/cnquery/utils/multierr"
    34  	"golang.org/x/crypto/ssh"
    35  	"golang.org/x/crypto/ssh/agent"
    36  	"golang.org/x/crypto/ssh/knownhosts"
    37  )
    38  
    39  const (
    40  	SSH shared.ConnectionType = "ssh"
    41  )
    42  
    43  type SshConnection struct {
    44  	id    uint32
    45  	conf  *inventory.Config
    46  	asset *inventory.Asset
    47  
    48  	fs   afero.Fs
    49  	Sudo *inventory.Sudo
    50  
    51  	serverVersion    string
    52  	UseScpFilesystem bool
    53  	HostKey          ssh.PublicKey
    54  	SSHClient        *ssh.Client
    55  }
    56  
    57  func NewSshConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*SshConnection, error) {
    58  	res := SshConnection{
    59  		id:    id,
    60  		conf:  conf,
    61  		asset: asset,
    62  	}
    63  
    64  	host := conf.GetHost()
    65  
    66  	// ipv6 addresses w/o the surrounding [] will eventually error
    67  	// so check whether we have an ipv6 address by parsing it (the
    68  	// parsing will fail if the string DOES have the []s) and adding
    69  	// the []s
    70  	ip := net.ParseIP(host)
    71  	if ip != nil && ip.To4() == nil {
    72  		conf.Host = "[" + host + "]"
    73  	}
    74  
    75  	conf = readSSHConfig(conf)
    76  	if err := verifyConfig(conf); err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	if os.Getenv("MONDOO_SSH_SCP") == "on" || conf.Options["ssh_scp"] == "on" {
    81  		res.UseScpFilesystem = true
    82  	}
    83  
    84  	if conf.Insecure {
    85  		log.Debug().Msg("user allowed insecure ssh connection")
    86  	}
    87  
    88  	if err := res.Connect(); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	// check uid of user and disable sudo if uid is 0
    93  	if conf.Sudo != nil && conf.Sudo.Active {
    94  		// the id command may not be available, eg. if ssh is used with windows
    95  		out, _ := res.RunCommand("id -u")
    96  		stdout, _ := io.ReadAll(out.Stdout)
    97  		// just check for the explicit positive case, otherwise just activate sudo
    98  		// we check sudo in VerifyConnection
    99  		if string(stdout) != "0" {
   100  			// configure sudo
   101  			log.Debug().Msg("activated sudo for ssh connection")
   102  			res.Sudo = conf.Sudo
   103  		}
   104  	}
   105  
   106  	// verify connection
   107  	vErr := res.verify()
   108  	// NOTE: for now we do not enforce connection verification to ensure we cover edge-cases
   109  	// TODO: in following minor version bumps, we want to enforce this behavior to ensure proper scans
   110  	if vErr != nil {
   111  		log.Warn().Err(vErr).Send()
   112  	}
   113  
   114  	return &res, nil
   115  }
   116  
   117  func (c *SshConnection) ID() uint32 {
   118  	return c.id
   119  }
   120  
   121  func (c *SshConnection) Name() string {
   122  	return "ssh"
   123  }
   124  
   125  func (c *SshConnection) Type() shared.ConnectionType {
   126  	return SSH
   127  }
   128  
   129  func (p *SshConnection) Asset() *inventory.Asset {
   130  	return p.asset
   131  }
   132  
   133  func (p *SshConnection) Capabilities() shared.Capabilities {
   134  	return shared.Capability_File | shared.Capability_RunCommand
   135  }
   136  
   137  func (c *SshConnection) RunCommand(command string) (*shared.Command, error) {
   138  	if c.Sudo != nil && c.Sudo.Active {
   139  		command = shared.BuildSudoCommand(c.Sudo, command)
   140  	}
   141  	return c.runRawCommand(command)
   142  }
   143  
   144  func (c *SshConnection) runRawCommand(command string) (*shared.Command, error) {
   145  	log.Debug().Str("command", command).Str("provider", "ssh").Msg("run command")
   146  
   147  	if c.SSHClient == nil {
   148  		return nil, errors.New("SSH session not established")
   149  	}
   150  
   151  	res := shared.Command{
   152  		Command: command,
   153  		Stats: shared.PerfStats{
   154  			Start: time.Now(),
   155  		},
   156  		Stdout: &bytes.Buffer{},
   157  		Stderr: &bytes.Buffer{},
   158  	}
   159  	defer func() {
   160  		res.Stats.Duration = time.Since(res.Stats.Start)
   161  	}()
   162  
   163  	session, err := c.SSHClient.NewSession()
   164  	if err != nil {
   165  		log.Debug().Msg("could not open new session, try to re-establish connection")
   166  
   167  		c.Close()
   168  		if err = c.Connect(); err != nil {
   169  			return nil, multierr.Wrap(err, "failed to open SSH session (reconnect failed)")
   170  		}
   171  
   172  		session, err = c.SSHClient.NewSession()
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  	}
   177  	defer session.Close()
   178  
   179  	// start ssh call
   180  	session.Stdout = res.Stdout
   181  	session.Stderr = res.Stderr
   182  	err = session.Run(res.Command)
   183  	if err == nil {
   184  		return &res, nil
   185  	}
   186  
   187  	// if the program failed, we do not return err but its exit code
   188  	var e *ssh.ExitError
   189  	match := errors.As(err, &e)
   190  	if match {
   191  		res.ExitStatus = e.ExitStatus()
   192  		return &res, nil
   193  	}
   194  
   195  	// all other errors are real errors and not expected
   196  	return &res, err
   197  }
   198  
   199  func (c *SshConnection) FileSystem() afero.Fs {
   200  	if c.fs != nil {
   201  		return c.fs
   202  	}
   203  
   204  	// log the used ssh filesystem backend
   205  	defer func() {
   206  		log.Debug().Str("file-transfer", c.fs.Name()).Msg("initialized ssh filesystem")
   207  	}()
   208  
   209  	//// detect cisco network gear, they returns something like SSH-2.0-Cisco-1.25
   210  	//// NOTE: we need to understand why this happens
   211  	//if strings.Contains(strings.ToLower(t.serverVersion), "cisco") {
   212  	//	log.Debug().Msg("detected cisco device, deactivate file system support")
   213  	//	t.fs = &fsutil.NoFs{}
   214  	//	return t.fs
   215  	//}
   216  
   217  	if c.Sudo != nil && c.Sudo.Active {
   218  		c.fs = cat.New(c)
   219  		return c.fs
   220  	}
   221  
   222  	// we always try to use sftp first (if scp is not user-enforced)
   223  	// and we also fallback to scp if sftp does not work
   224  	if !c.UseScpFilesystem {
   225  		fs, err := sftp.New(c, c.SSHClient)
   226  		if err != nil {
   227  			log.Info().Msg("use scp instead of sftp")
   228  			// enable fallback
   229  			c.UseScpFilesystem = true
   230  		} else {
   231  			c.fs = fs
   232  			return c.fs
   233  		}
   234  	}
   235  
   236  	if c.UseScpFilesystem {
   237  		c.fs = scp.NewFs(c, c.SSHClient)
   238  		return c.fs
   239  	}
   240  
   241  	// always fallback to catfs, slow but it works
   242  	c.fs = cat.New(c)
   243  	return c.fs
   244  }
   245  
   246  func (c *SshConnection) FileInfo(path string) (shared.FileInfoDetails, error) {
   247  	fs := c.FileSystem()
   248  	afs := &afero.Afero{Fs: fs}
   249  	stat, err := afs.Stat(path)
   250  	if err != nil {
   251  		return shared.FileInfoDetails{}, err
   252  	}
   253  
   254  	uid := int64(-1)
   255  	gid := int64(-1)
   256  
   257  	if c.Sudo != nil || c.UseScpFilesystem {
   258  		if stat, ok := stat.Sys().(*shared.FileInfo); ok {
   259  			uid = int64(stat.Uid)
   260  			gid = int64(stat.Gid)
   261  		}
   262  	} else {
   263  		if stat, ok := stat.Sys().(*rawsftp.FileStat); ok {
   264  			uid = int64(stat.UID)
   265  			gid = int64(stat.GID)
   266  		}
   267  	}
   268  	mode := stat.Mode()
   269  
   270  	return shared.FileInfoDetails{
   271  		Mode: shared.FileModeDetails{mode},
   272  		Size: stat.Size(),
   273  		Uid:  uid,
   274  		Gid:  gid,
   275  	}, nil
   276  }
   277  
   278  func (c *SshConnection) Close() {
   279  	if c.SSHClient != nil {
   280  		c.SSHClient.Close()
   281  	}
   282  }
   283  
   284  func (c *SshConnection) Connect() error {
   285  	cc := c.conf
   286  
   287  	// we always want to ensure we use the default port if nothing was specified
   288  	if cc.Port == 0 {
   289  		cc.Port = 22
   290  	}
   291  
   292  	// load known hosts and track the fingerprint of the ssh server for later identification
   293  	knownHostsCallback, err := knownHostsCallback()
   294  	if err != nil {
   295  		return multierr.Wrap(err, "could not read hostkey file")
   296  	}
   297  
   298  	var hostkey ssh.PublicKey
   299  	hostkeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
   300  		// store the hostkey for later identification
   301  		hostkey = key
   302  
   303  		// ignore hostkey check if the user provided an insecure flag
   304  		if cc.Insecure {
   305  			return nil
   306  		}
   307  
   308  		// knownhost.New returns a ssh.CertChecker which does not work with all ssh.HostKey types
   309  		// especially the newer edcsa keys (ssh.curve25519sha256) are not well supported.
   310  		// https://github.com/golang/crypto/blob/master/ssh/knownhosts/knownhosts.go#L417-L436
   311  		// creates the CertChecker which requires an instance of Certificate
   312  		// https://github.com/golang/crypto/blob/master/ssh/certs.go#L326-L348
   313  		// https://github.com/golang/crypto/blob/master/ssh/keys.go#L271-L283
   314  		// therefore it is best to skip the checking for now since it forces users to set the insecure flag otherwise
   315  		// TODO: implement custom host-key checking for normal public keys as well
   316  		_, ok := key.(*ssh.Certificate)
   317  		if !ok {
   318  			log.Debug().Msg("skip hostkey check the hostkey since the algo is not supported yet")
   319  			return nil
   320  		}
   321  
   322  		err := knownHostsCallback(hostname, remote, key)
   323  		if err != nil {
   324  			log.Debug().Err(err).Str("hostname", hostname).Str("ip", remote.String()).Msg("check known host")
   325  		}
   326  		return err
   327  	}
   328  
   329  	// establish connection
   330  	conn, _, err := establishClientConnection(cc, hostkeyCallback)
   331  	if err != nil {
   332  		log.Debug().Err(err).Str("provider", "ssh").Str("host", cc.Host).Int32("port", cc.Port).Bool("insecure", cc.Insecure).Msg("could not establish ssh session")
   333  		if strings.ContainsAny(cc.Host, "[]") {
   334  			log.Info().Str("host", cc.Host).Int32("port", cc.Port).Msg("ensure proper []s when combining IPv6 with port numbers")
   335  		}
   336  		return err
   337  	}
   338  	c.SSHClient = conn
   339  	c.HostKey = hostkey
   340  	c.serverVersion = string(conn.ServerVersion())
   341  	log.Debug().Str("provider", "ssh").Str("host", cc.Host).Int32("port", cc.Port).Str("server", c.serverVersion).Msg("ssh session established")
   342  	return nil
   343  }
   344  
   345  func (c *SshConnection) PlatformID() (string, error) {
   346  	return PlatformIdentifier(c.HostKey), nil
   347  }
   348  
   349  func PlatformIdentifier(publicKey ssh.PublicKey) string {
   350  	fingerprint := ssh.FingerprintSHA256(publicKey)
   351  	fingerprint = strings.Replace(fingerprint, ":", "-", 1)
   352  	identifier := "//platformid.api.mondoo.app/runtime/ssh/hostkey/" + fingerprint
   353  	return identifier
   354  }
   355  
   356  func readSSHConfig(cc *inventory.Config) *inventory.Config {
   357  	host := cc.Host
   358  
   359  	home, err := homedir.Dir()
   360  	if err != nil {
   361  		log.Debug().Err(err).Msg("ssh> failed to determine user home directory")
   362  		return cc
   363  	}
   364  
   365  	sshUserConfigPath := filepath.Join(home, ".ssh", "config")
   366  	f, err := os.Open(sshUserConfigPath)
   367  	if err != nil {
   368  		log.Debug().Err(err).Str("file", sshUserConfigPath).Msg("ssh> could not read ssh config")
   369  		return cc
   370  	}
   371  
   372  	cfg, err := ssh_config.Decode(f)
   373  	if err != nil {
   374  		log.Debug().Err(err).Str("file", sshUserConfigPath).Msg("could not parse ssh config")
   375  		return cc
   376  	}
   377  
   378  	// optional step, tries to parse the ssh config to see if additional information
   379  	// is already available
   380  	hostname, err := cfg.Get(host, "HostName")
   381  	if err == nil && len(hostname) > 0 {
   382  		cc.Host = hostname
   383  	}
   384  
   385  	if len(cc.Credentials) == 0 || (len(cc.Credentials) == 1 && cc.Credentials[0].Type == vault.CredentialType_password && len(cc.Credentials[0].Secret) == 0) {
   386  		user, _ := cfg.Get(host, "User")
   387  		port, err := cfg.Get(host, "Port")
   388  		if err == nil {
   389  			portNum, err := strconv.Atoi(port)
   390  			if err != nil {
   391  				log.Debug().Err(err).Str("file", sshUserConfigPath).Str("port", port).Msg("could not parse ssh port")
   392  			} else {
   393  				cc.Port = int32(portNum)
   394  			}
   395  		}
   396  
   397  		entry, err := cfg.Get(host, "IdentityFile")
   398  
   399  		// TODO: the ssh_config uses os/home but instead should be use go-homedir, could become a compile issue
   400  		// TODO: the problem is that the lib returns defaults and we cannot properly distingush
   401  		if err == nil && ssh_config.Default("IdentityFile") != entry {
   402  			// commonly ssh config included paths like ~
   403  			expandedPath, err := homedir.Expand(entry)
   404  			if err == nil {
   405  				log.Debug().Str("key", expandedPath).Str("host", host).Msg("ssh> read ssh identity key from ssh config")
   406  				// NOTE: we ignore the error here for now but this should probably been caught earlier anyway
   407  				credential, _ := vault.NewPrivateKeyCredentialFromPath(user, expandedPath, "")
   408  				// apply the option manually
   409  				if credential != nil {
   410  					cc.Credentials = append(cc.Credentials, credential)
   411  				}
   412  			}
   413  		}
   414  	}
   415  
   416  	// handle disable of strict hostkey checking:
   417  	// Host *
   418  	// StrictHostKeyChecking no
   419  	entry, err := cfg.Get(host, "StrictHostKeyChecking")
   420  	if err == nil && strings.ToLower(entry) == "no" {
   421  		cc.Insecure = true
   422  	}
   423  	return cc
   424  }
   425  
   426  func verifyConfig(conf *inventory.Config) error {
   427  	if conf.Type != "ssh" {
   428  		return inventory.ErrProviderTypeDoesNotMatch
   429  	}
   430  
   431  	return nil
   432  }
   433  
   434  func knownHostsCallback() (ssh.HostKeyCallback, error) {
   435  	home, err := homedir.Dir()
   436  	if err != nil {
   437  		log.Debug().Err(err).Msg("Failed to determine user home directory")
   438  		return nil, err
   439  	}
   440  
   441  	// load default host keys
   442  	files := []string{
   443  		filepath.Join(home, ".ssh", "known_hosts"),
   444  		// see https://cloud.google.com/compute/docs/instances/connecting-to-instance
   445  		// NOTE: content in that file is structured by compute.instanceid key
   446  		// TODO: we need to keep the instance information during the resolve step
   447  		filepath.Join(home, ".ssh", "google_compute_known_hosts"),
   448  	}
   449  
   450  	// filter all files that do not exits
   451  	existentKnownHosts := []string{}
   452  	for i := range files {
   453  		_, err := os.Stat(files[i])
   454  		if err == nil {
   455  			log.Debug().Str("file", files[i]).Msg("load ssh known_hosts file")
   456  			existentKnownHosts = append(existentKnownHosts, files[i])
   457  		}
   458  	}
   459  
   460  	return knownhosts.New(existentKnownHosts...)
   461  }
   462  
   463  func establishClientConnection(pCfg *inventory.Config, hostKeyCallback ssh.HostKeyCallback) (*ssh.Client, []io.Closer, error) {
   464  	authMethods, closer, err := prepareConnection(pCfg)
   465  	if err != nil {
   466  		return nil, nil, err
   467  	}
   468  
   469  	if len(authMethods) == 0 {
   470  		return nil, nil, errors.New("no authentication method defined")
   471  	}
   472  
   473  	// TODO: hack: we want to establish a proper connection per configured connection so that we could use multiple users
   474  	user := ""
   475  	for i := range pCfg.Credentials {
   476  		if pCfg.Credentials[i].User != "" {
   477  			user = pCfg.Credentials[i].User
   478  		}
   479  	}
   480  
   481  	log.Debug().Int("methods", len(authMethods)).Str("user", user).Msg("connect to remote ssh")
   482  	conn, err := ssh.Dial("tcp", pCfg.Host+":"+strconv.Itoa(int(pCfg.Port)), &ssh.ClientConfig{
   483  		User:            user,
   484  		Auth:            authMethods,
   485  		HostKeyCallback: hostKeyCallback,
   486  	})
   487  	return conn, closer, err
   488  }
   489  
   490  // hasAgentLoadedKey returns if the ssh agent has loaded the key file
   491  // This may not be 100% accurate. The key can be stored in multiple locations with the
   492  // same fingerprint. We cannot determine the fingerprint without decoding the encrypted
   493  // key, `ssh-keygen -lf /Users/chartmann/.ssh/id_rsa` seems to use the ssh agent to
   494  // determine the fingerprint without prompting for the password
   495  func hasAgentLoadedKey(list []*agent.Key, filename string) bool {
   496  	for i := range list {
   497  		if list[i].Comment == filename {
   498  			return true
   499  		}
   500  	}
   501  	return false
   502  }
   503  
   504  // prepareConnection determines the auth methods required for a ssh connection and also prepares any other
   505  // pre-conditions for the connection like tunnelling the connection via AWS SSM session
   506  func prepareConnection(conf *inventory.Config) ([]ssh.AuthMethod, []io.Closer, error) {
   507  	auths := []ssh.AuthMethod{}
   508  	closer := []io.Closer{}
   509  
   510  	// only one public auth method is allowed, therefore multiple keys need to be encapsulated into one auth method
   511  	sshSigners := []ssh.Signer{}
   512  
   513  	// if no credential was provided, fallback to ssh-agent and ssh-config
   514  	if len(conf.Credentials) == 0 {
   515  		sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
   516  	}
   517  
   518  	// use key auth, only load if the key was not found in ssh agent
   519  	for i := range conf.Credentials {
   520  		credential := conf.Credentials[i]
   521  
   522  		switch credential.Type {
   523  		case vault.CredentialType_private_key:
   524  			log.Debug().Msg("enabled ssh private key authentication")
   525  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(credential.Secret, []byte(credential.Password))
   526  			if err != nil {
   527  				log.Debug().Err(err).Msg("could not read private key")
   528  			} else {
   529  				sshSigners = append(sshSigners, priv)
   530  			}
   531  		case vault.CredentialType_password:
   532  			// use password auth if the password was set, this is also used when only the username is set
   533  			if len(credential.Secret) > 0 {
   534  				log.Debug().Msg("enabled ssh password authentication")
   535  				auths = append(auths, ssh.Password(string(credential.Secret)))
   536  			}
   537  		case vault.CredentialType_ssh_agent:
   538  			log.Debug().Msg("enabled ssh agent authentication")
   539  			sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
   540  		case vault.CredentialType_aws_ec2_ssm_session:
   541  			// when the user establishes the ssm session we do the following
   542  			// 1. start websocket connection and start the session-manager-plugin to map the websocket to a local port
   543  			// 2. create new ssh key via instance connect so that we do not rely on any pre-existing ssh key
   544  			err := awsssmsession.CheckPlugin()
   545  			if err != nil {
   546  				return nil, nil, errors.New("Local AWS Session Manager plugin is missing. See https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html for information on the AWS Session Manager plugin and installation instructions")
   547  			}
   548  
   549  			loadOpts := []func(*awsconf.LoadOptions) error{}
   550  			if conf.Options != nil && conf.Options["region"] != "" {
   551  				loadOpts = append(loadOpts, awsconf.WithRegion(conf.Options["region"]))
   552  			}
   553  			profile := ""
   554  			if conf.Options != nil && conf.Options["profile"] != "" {
   555  				loadOpts = append(loadOpts, awsconf.WithSharedConfigProfile(conf.Options["profile"]))
   556  				profile = conf.Options["profile"]
   557  			}
   558  			log.Debug().Str("profile", conf.Options["profile"]).Str("region", conf.Options["region"]).Msg("using aws creds")
   559  
   560  			cfg, err := awsconf.LoadDefaultConfig(context.Background(), loadOpts...)
   561  			if err != nil {
   562  				return nil, nil, err
   563  			}
   564  
   565  			// we use ec2 instance connect api to create credentials for an aws instance
   566  			eic := awsinstanceconnect.New(cfg)
   567  			host := conf.Host
   568  			if id, ok := conf.Options["instance"]; ok {
   569  				host = id
   570  			}
   571  			creds, err := eic.GenerateCredentials(host, credential.User)
   572  			if err != nil {
   573  				return nil, nil, err
   574  			}
   575  
   576  			// we use ssm session manager to connect to instance via websockets
   577  			sManager, err := awsssmsession.NewAwsSsmSessionManager(cfg, profile)
   578  			if err != nil {
   579  				return nil, nil, err
   580  			}
   581  
   582  			// prepare websocket connection and bind it to a free local port
   583  			localIp := "localhost"
   584  			remotePort := "22"
   585  			// NOTE: for SSM we always target the instance id
   586  			conf.Host = creds.InstanceId
   587  			localPort, err := awsssmsession.GetAvailablePort()
   588  			if err != nil {
   589  				return nil, nil, errors.New("could not find an available port to start the ssm proxy")
   590  			}
   591  			ssmConn, err := sManager.Dial(conf, strconv.Itoa(localPort), remotePort)
   592  			if err != nil {
   593  				return nil, nil, err
   594  			}
   595  
   596  			// update endpoint information for ssh to connect via local ssm proxy
   597  			// TODO: this has a side-effect, we may need extend the struct to include resolved connection data
   598  			conf.Host = localIp
   599  			conf.Port = int32(localPort)
   600  
   601  			// NOTE: we need to set insecure so that ssh does not complain about the host key
   602  			// It is okay do that since the connection is established via aws api itself and it ensures that
   603  			// the instance id is okay
   604  			conf.Insecure = true
   605  
   606  			// use the generated ssh credentials for authentication
   607  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
   608  			if err != nil {
   609  				return nil, nil, multierr.Wrap(err, "could not read generated private key")
   610  			}
   611  			sshSigners = append(sshSigners, priv)
   612  			closer = append(closer, ssmConn)
   613  		case vault.CredentialType_aws_ec2_instance_connect:
   614  			log.Debug().Str("profile", conf.Options["profile"]).Str("region", conf.Options["region"]).Msg("using aws creds")
   615  
   616  			loadOpts := []func(*awsconf.LoadOptions) error{}
   617  			if conf.Options != nil && conf.Options["region"] != "" {
   618  				loadOpts = append(loadOpts, awsconf.WithRegion(conf.Options["region"]))
   619  			}
   620  			if conf.Options != nil && conf.Options["profile"] != "" {
   621  				loadOpts = append(loadOpts, awsconf.WithSharedConfigProfile(conf.Options["profile"]))
   622  			}
   623  			cfg, err := awsconf.LoadDefaultConfig(context.Background(), loadOpts...)
   624  			if err != nil {
   625  				return nil, nil, err
   626  			}
   627  			log.Debug().Msg("generating instance connect credentials")
   628  			eic := awsinstanceconnect.New(cfg)
   629  			host := conf.Host
   630  			if id, ok := conf.Options["instance"]; ok {
   631  				host = id
   632  			}
   633  			creds, err := eic.GenerateCredentials(host, credential.User)
   634  			if err != nil {
   635  				return nil, nil, err
   636  			}
   637  
   638  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
   639  			if err != nil {
   640  				return nil, nil, multierr.Wrap(err, "could not read generated private key")
   641  			}
   642  			sshSigners = append(sshSigners, priv)
   643  
   644  			// NOTE: this creates a side-effect where the host is overwritten
   645  			conf.Host = creds.PublicIpAddress
   646  		default:
   647  			return nil, nil, errors.New("unsupported authentication mechanism for ssh: " + credential.Type.String())
   648  		}
   649  	}
   650  
   651  	if len(sshSigners) > 0 {
   652  		auths = append(auths, ssh.PublicKeys(sshSigners...))
   653  	}
   654  
   655  	return auths, closer, nil
   656  }
   657  
   658  func (c *SshConnection) verify() error {
   659  	var out *shared.Command
   660  	var err error
   661  	if c.Sudo != nil {
   662  		// Wrap sudo command, to see proper error messages. We set /dev/null to disable stdin
   663  		command := "sh -c '" + shared.BuildSudoCommand(c.Sudo, "echo 'hi'") + " < /dev/null'"
   664  		out, err = c.runRawCommand(command)
   665  	} else {
   666  		out, err = c.runRawCommand("echo 'hi'")
   667  	}
   668  	if err != nil {
   669  		return err
   670  	}
   671  
   672  	if out.ExitStatus == 0 {
   673  		return nil
   674  	}
   675  
   676  	stderr, _ := io.ReadAll(out.Stderr)
   677  	errMsg := string(stderr)
   678  
   679  	// sample messages are:
   680  	// sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
   681  	// sudo: a password is required
   682  	switch {
   683  	case strings.Contains(errMsg, "not found"):
   684  		return errors.New("sudo command is missing on target")
   685  	case strings.Contains(errMsg, "a password is required"):
   686  		return errors.New("could not establish connection: sudo password is not supported yet, configure password-less sudo")
   687  	default:
   688  		return errors.New("could not establish connection: " + errMsg)
   689  	}
   690  }