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 }