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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package ssh
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"strconv"
    11  
    12  	"github.com/aws/aws-sdk-go-v2/config"
    13  	"github.com/cockroachdb/errors"
    14  	"github.com/rs/zerolog/log"
    15  	"go.mondoo.com/cnquery/motor/providers"
    16  	"go.mondoo.com/cnquery/motor/providers/ssh/awsinstanceconnect"
    17  	"go.mondoo.com/cnquery/motor/providers/ssh/awsssmsession"
    18  	"go.mondoo.com/cnquery/motor/providers/ssh/signers"
    19  	"go.mondoo.com/cnquery/motor/vault"
    20  	"golang.org/x/crypto/ssh"
    21  	"golang.org/x/crypto/ssh/agent"
    22  )
    23  
    24  func establishClientConnection(pCfg *providers.Config, hostKeyCallback ssh.HostKeyCallback) (*ssh.Client, []io.Closer, error) {
    25  	authMethods, closer, err := prepareConnection(pCfg)
    26  	if err != nil {
    27  		return nil, nil, err
    28  	}
    29  
    30  	if len(authMethods) == 0 {
    31  		return nil, nil, errors.New("no authentication method defined")
    32  	}
    33  
    34  	// TODO: hack: we want to establish a proper connection per configured connection so that we could use multiple users
    35  	user := ""
    36  	for i := range pCfg.Credentials {
    37  		if pCfg.Credentials[i].User != "" {
    38  			user = pCfg.Credentials[i].User
    39  		}
    40  	}
    41  
    42  	log.Debug().Int("methods", len(authMethods)).Str("user", user).Msg("connect to remote ssh")
    43  	conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", pCfg.Host, pCfg.Port), &ssh.ClientConfig{
    44  		User:            user,
    45  		Auth:            authMethods,
    46  		HostKeyCallback: hostKeyCallback,
    47  	})
    48  	return conn, closer, err
    49  }
    50  
    51  // hasAgentLoadedKey returns if the ssh agent has loaded the key file
    52  // This may not be 100% accurate. The key can be stored in multiple locations with the
    53  // same fingerprint. We cannot determine the fingerprint without decoding the encrypted
    54  // key, `ssh-keygen -lf /Users/chartmann/.ssh/id_rsa` seems to use the ssh agent to
    55  // determine the fingerprint without prompting for the password
    56  func hasAgentLoadedKey(list []*agent.Key, filename string) bool {
    57  	for i := range list {
    58  		if list[i].Comment == filename {
    59  			return true
    60  		}
    61  	}
    62  	return false
    63  }
    64  
    65  // prepareConnection determines the auth methods required for a ssh connection and also prepares any other
    66  // pre-conditions for the connection like tunnelling the connection via AWS SSM session
    67  func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, error) {
    68  	auths := []ssh.AuthMethod{}
    69  	closer := []io.Closer{}
    70  
    71  	// only one public auth method is allowed, therefore multiple keys need to be encapsulated into one auth method
    72  	sshSigners := []ssh.Signer{}
    73  
    74  	// use key auth, only load if the key was not found in ssh agent
    75  	for i := range pCfg.Credentials {
    76  		credential := pCfg.Credentials[i]
    77  
    78  		switch credential.Type {
    79  		case vault.CredentialType_private_key:
    80  			log.Debug().Msg("enabled ssh private key authentication")
    81  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(credential.Secret, []byte(credential.Password))
    82  			if err != nil {
    83  				log.Debug().Err(err).Msg("could not read private key")
    84  			} else {
    85  				sshSigners = append(sshSigners, priv)
    86  			}
    87  		case vault.CredentialType_password:
    88  			// use password auth if the password was set, this is also used when only the username is set
    89  			if len(credential.Secret) > 0 {
    90  				log.Debug().Msg("enabled ssh password authentication")
    91  				auths = append(auths, ssh.Password(string(credential.Secret)))
    92  			}
    93  		case vault.CredentialType_ssh_agent:
    94  			log.Debug().Msg("enabled ssh agent authentication")
    95  			sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
    96  		case vault.CredentialType_aws_ec2_ssm_session:
    97  			// when the user establishes the ssm session we do the following
    98  			// 1. start websocket connection and start the session-manager-plugin to map the websocket to a local port
    99  			// 2. create new ssh key via instance connect so that we do not rely on any pre-existing ssh key
   100  			err := awsssmsession.CheckPlugin()
   101  			if err != nil {
   102  				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")
   103  			}
   104  
   105  			loadOpts := []func(*config.LoadOptions) error{}
   106  			if pCfg.Options != nil && pCfg.Options["region"] != "" {
   107  				loadOpts = append(loadOpts, config.WithRegion(pCfg.Options["region"]))
   108  			}
   109  			profile := ""
   110  			if pCfg.Options != nil && pCfg.Options["profile"] != "" {
   111  				loadOpts = append(loadOpts, config.WithSharedConfigProfile(pCfg.Options["profile"]))
   112  				profile = pCfg.Options["profile"]
   113  			}
   114  			log.Debug().Str("profile", pCfg.Options["profile"]).Str("region", pCfg.Options["region"]).Msg("using aws creds")
   115  
   116  			cfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...)
   117  			if err != nil {
   118  				return nil, nil, err
   119  			}
   120  
   121  			// we use ec2 instance connect api to create credentials for an aws instance
   122  			eic := awsinstanceconnect.New(cfg)
   123  			host := pCfg.Host
   124  			if id, ok := pCfg.Options["instance"]; ok {
   125  				host = id
   126  			}
   127  			creds, err := eic.GenerateCredentials(host, credential.User)
   128  			if err != nil {
   129  				return nil, nil, err
   130  			}
   131  
   132  			// we use ssm session manager to connect to instance via websockets
   133  			sManager, err := awsssmsession.NewAwsSsmSessionManager(cfg, profile)
   134  			if err != nil {
   135  				return nil, nil, err
   136  			}
   137  
   138  			// prepare websocket connection and bind it to a free local port
   139  			localIp := "localhost"
   140  			remotePort := "22"
   141  			// NOTE: for SSM we always target the instance id
   142  			pCfg.Host = creds.InstanceId
   143  			localPort, err := awsssmsession.GetAvailablePort()
   144  			if err != nil {
   145  				return nil, nil, errors.New("could not find an available port to start the ssm proxy")
   146  			}
   147  			ssmConn, err := sManager.Dial(pCfg, strconv.Itoa(localPort), remotePort)
   148  			if err != nil {
   149  				return nil, nil, err
   150  			}
   151  
   152  			// update endpoint information for ssh to connect via local ssm proxy
   153  			// TODO: this has a side-effect, we may need extend the struct to include resolved connection data
   154  			pCfg.Host = localIp
   155  			pCfg.Port = int32(localPort)
   156  
   157  			// NOTE: we need to set insecure so that ssh does not complain about the host key
   158  			// It is okay do that since the connection is established via aws api itself and it ensures that
   159  			// the instance id is okay
   160  			pCfg.Insecure = true
   161  
   162  			// use the generated ssh credentials for authentication
   163  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
   164  			if err != nil {
   165  				return nil, nil, errors.Wrap(err, "could not read generated private key")
   166  			}
   167  			sshSigners = append(sshSigners, priv)
   168  			closer = append(closer, ssmConn)
   169  		case vault.CredentialType_aws_ec2_instance_connect:
   170  			log.Debug().Str("profile", pCfg.Options["profile"]).Str("region", pCfg.Options["region"]).Msg("using aws creds")
   171  
   172  			loadOpts := []func(*config.LoadOptions) error{}
   173  			if pCfg.Options != nil && pCfg.Options["region"] != "" {
   174  				loadOpts = append(loadOpts, config.WithRegion(pCfg.Options["region"]))
   175  			}
   176  			if pCfg.Options != nil && pCfg.Options["profile"] != "" {
   177  				loadOpts = append(loadOpts, config.WithSharedConfigProfile(pCfg.Options["profile"]))
   178  			}
   179  			cfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...)
   180  			if err != nil {
   181  				return nil, nil, err
   182  			}
   183  			log.Debug().Msg("generating instance connect credentials")
   184  			eic := awsinstanceconnect.New(cfg)
   185  			host := pCfg.Host
   186  			if id, ok := pCfg.Options["instance"]; ok {
   187  				host = id
   188  			}
   189  			creds, err := eic.GenerateCredentials(host, credential.User)
   190  			if err != nil {
   191  				return nil, nil, err
   192  			}
   193  
   194  			priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
   195  			if err != nil {
   196  				return nil, nil, errors.Wrap(err, "could not read generated private key")
   197  			}
   198  			sshSigners = append(sshSigners, priv)
   199  
   200  			// NOTE: this creates a side-effect where the host is overwritten
   201  			pCfg.Host = creds.PublicIpAddress
   202  		default:
   203  			return nil, nil, errors.New("unsupported authentication mechanism for ssh: " + credential.Type.String())
   204  		}
   205  	}
   206  
   207  	if len(sshSigners) > 0 {
   208  		auths = append(auths, ssh.PublicKeys(sshSigners...))
   209  	}
   210  
   211  	// if no credential was provided, fallback to ssh-agent and ssh-config
   212  	if len(pCfg.Credentials) == 0 {
   213  		sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
   214  	}
   215  
   216  	return auths, closer, nil
   217  }