github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/communicator/ssh/provisioner.go (about) 1 package ssh 2 3 import ( 4 "bytes" 5 "encoding/pem" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "net" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/hashicorp/terraform/communicator/shared" 17 "github.com/hashicorp/terraform/terraform" 18 "github.com/mitchellh/mapstructure" 19 "github.com/xanzy/ssh-agent" 20 "golang.org/x/crypto/ssh" 21 "golang.org/x/crypto/ssh/agent" 22 "golang.org/x/crypto/ssh/knownhosts" 23 ) 24 25 const ( 26 // DefaultUser is used if there is no user given 27 DefaultUser = "root" 28 29 // DefaultPort is used if there is no port given 30 DefaultPort = 22 31 32 // DefaultScriptPath is used as the path to copy the file to 33 // for remote execution if not provided otherwise. 34 DefaultScriptPath = "/tmp/terraform_%RAND%.sh" 35 36 // DefaultTimeout is used if there is no timeout given 37 DefaultTimeout = 5 * time.Minute 38 ) 39 40 // connectionInfo is decoded from the ConnInfo of the resource. These are the 41 // only keys we look at. If a PrivateKey is given, that is used instead 42 // of a password. 43 type connectionInfo struct { 44 User string 45 Password string 46 PrivateKey string `mapstructure:"private_key"` 47 Certificate string `mapstructure:"certificate"` 48 Host string 49 HostKey string `mapstructure:"host_key"` 50 Port int 51 Agent bool 52 Timeout string 53 ScriptPath string `mapstructure:"script_path"` 54 TimeoutVal time.Duration `mapstructure:"-"` 55 56 BastionUser string `mapstructure:"bastion_user"` 57 BastionPassword string `mapstructure:"bastion_password"` 58 BastionPrivateKey string `mapstructure:"bastion_private_key"` 59 BastionCertificate string `mapstructure:"bastion_certificate"` 60 BastionHost string `mapstructure:"bastion_host"` 61 BastionHostKey string `mapstructure:"bastion_host_key"` 62 BastionPort int `mapstructure:"bastion_port"` 63 64 AgentIdentity string `mapstructure:"agent_identity"` 65 } 66 67 // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into 68 // a ConnectionInfo struct 69 func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { 70 connInfo := &connectionInfo{} 71 decConf := &mapstructure.DecoderConfig{ 72 WeaklyTypedInput: true, 73 Result: connInfo, 74 } 75 dec, err := mapstructure.NewDecoder(decConf) 76 if err != nil { 77 return nil, err 78 } 79 if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil { 80 return nil, err 81 } 82 83 // To default Agent to true, we need to check the raw string, since the 84 // decoded boolean can't represent "absence of config". 85 // 86 // And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we 87 // shouldn't try. 88 if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" { 89 connInfo.Agent = true 90 } 91 92 if connInfo.User == "" { 93 connInfo.User = DefaultUser 94 } 95 96 // Format the host if needed. 97 // Needed for IPv6 support. 98 connInfo.Host = shared.IpFormat(connInfo.Host) 99 100 if connInfo.Port == 0 { 101 connInfo.Port = DefaultPort 102 } 103 if connInfo.ScriptPath == "" { 104 connInfo.ScriptPath = DefaultScriptPath 105 } 106 if connInfo.Timeout != "" { 107 connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout) 108 } else { 109 connInfo.TimeoutVal = DefaultTimeout 110 } 111 112 // Default all bastion config attrs to their non-bastion counterparts 113 if connInfo.BastionHost != "" { 114 // Format the bastion host if needed. 115 // Needed for IPv6 support. 116 connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost) 117 118 if connInfo.BastionUser == "" { 119 connInfo.BastionUser = connInfo.User 120 } 121 if connInfo.BastionPassword == "" { 122 connInfo.BastionPassword = connInfo.Password 123 } 124 if connInfo.BastionPrivateKey == "" { 125 connInfo.BastionPrivateKey = connInfo.PrivateKey 126 } 127 if connInfo.BastionCertificate == "" { 128 connInfo.BastionCertificate = connInfo.Certificate 129 } 130 if connInfo.BastionPort == 0 { 131 connInfo.BastionPort = connInfo.Port 132 } 133 } 134 135 return connInfo, nil 136 } 137 138 // safeDuration returns either the parsed duration or a default value 139 func safeDuration(dur string, defaultDur time.Duration) time.Duration { 140 d, err := time.ParseDuration(dur) 141 if err != nil { 142 log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) 143 return defaultDur 144 } 145 return d 146 } 147 148 // prepareSSHConfig is used to turn the *ConnectionInfo provided into a 149 // usable *SSHConfig for client initialization. 150 func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) { 151 sshAgent, err := connectToAgent(connInfo) 152 if err != nil { 153 return nil, err 154 } 155 156 host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port) 157 158 sshConf, err := buildSSHClientConfig(sshClientConfigOpts{ 159 user: connInfo.User, 160 host: host, 161 privateKey: connInfo.PrivateKey, 162 password: connInfo.Password, 163 hostKey: connInfo.HostKey, 164 certificate: connInfo.Certificate, 165 sshAgent: sshAgent, 166 }) 167 if err != nil { 168 return nil, err 169 } 170 171 connectFunc := ConnectFunc("tcp", host) 172 173 var bastionConf *ssh.ClientConfig 174 if connInfo.BastionHost != "" { 175 bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort) 176 177 bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{ 178 user: connInfo.BastionUser, 179 host: bastionHost, 180 privateKey: connInfo.BastionPrivateKey, 181 password: connInfo.BastionPassword, 182 hostKey: connInfo.HostKey, 183 certificate: connInfo.BastionCertificate, 184 sshAgent: sshAgent, 185 }) 186 if err != nil { 187 return nil, err 188 } 189 190 connectFunc = BastionConnectFunc("tcp", bastionHost, bastionConf, "tcp", host) 191 } 192 193 config := &sshConfig{ 194 config: sshConf, 195 connection: connectFunc, 196 sshAgent: sshAgent, 197 } 198 return config, nil 199 } 200 201 type sshClientConfigOpts struct { 202 privateKey string 203 password string 204 sshAgent *sshAgent 205 certificate string 206 user string 207 host string 208 hostKey string 209 } 210 211 func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { 212 hkCallback := ssh.InsecureIgnoreHostKey() 213 214 if opts.hostKey != "" { 215 // The knownhosts package only takes paths to files, but terraform 216 // generally wants to handle config data in-memory. Rather than making 217 // the known_hosts file an exception, write out the data to a temporary 218 // file to create the HostKeyCallback. 219 tf, err := ioutil.TempFile("", "tf-known_hosts") 220 if err != nil { 221 return nil, fmt.Errorf("failed to create temp known_hosts file: %s", err) 222 } 223 defer tf.Close() 224 defer os.RemoveAll(tf.Name()) 225 226 // we mark this as a CA as well, but the host key fallback will still 227 // use it as a direct match if the remote host doesn't return a 228 // certificate. 229 if _, err := tf.WriteString(fmt.Sprintf("@cert-authority %s %s\n", opts.host, opts.hostKey)); err != nil { 230 return nil, fmt.Errorf("failed to write temp known_hosts file: %s", err) 231 } 232 tf.Sync() 233 234 hkCallback, err = knownhosts.New(tf.Name()) 235 if err != nil { 236 return nil, err 237 } 238 } 239 240 conf := &ssh.ClientConfig{ 241 HostKeyCallback: hkCallback, 242 User: opts.user, 243 } 244 245 if opts.privateKey != "" { 246 if opts.certificate != "" { 247 log.Println("using client certificate for authentication") 248 249 certSigner, err := signCertWithPrivateKey(opts.privateKey, opts.certificate) 250 if err != nil { 251 return nil, err 252 } 253 conf.Auth = append(conf.Auth, certSigner) 254 } else { 255 log.Println("using private key for authentication") 256 257 pubKeyAuth, err := readPrivateKey(opts.privateKey) 258 if err != nil { 259 return nil, err 260 } 261 conf.Auth = append(conf.Auth, pubKeyAuth) 262 } 263 } 264 265 if opts.password != "" { 266 conf.Auth = append(conf.Auth, ssh.Password(opts.password)) 267 conf.Auth = append(conf.Auth, ssh.KeyboardInteractive( 268 PasswordKeyboardInteractive(opts.password))) 269 } 270 271 if opts.sshAgent != nil { 272 conf.Auth = append(conf.Auth, opts.sshAgent.Auth()) 273 } 274 275 return conf, nil 276 } 277 278 // Create a Cert Signer and return ssh.AuthMethod 279 func signCertWithPrivateKey(pk string, certificate string) (ssh.AuthMethod, error) { 280 rawPk, err := ssh.ParseRawPrivateKey([]byte(pk)) 281 if err != nil { 282 return nil, fmt.Errorf("failed to parse private key %q: %s", pk, err) 283 } 284 285 pcert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate)) 286 if err != nil { 287 return nil, fmt.Errorf("failed to parse certificate %q: %s", certificate, err) 288 } 289 290 usigner, err := ssh.NewSignerFromKey(rawPk) 291 if err != nil { 292 return nil, fmt.Errorf("failed to create signer from raw private key %q: %s", rawPk, err) 293 } 294 295 ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner) 296 if err != nil { 297 return nil, fmt.Errorf("failed to create cert signer %q: %s", usigner, err) 298 } 299 300 return ssh.PublicKeys(ucertSigner), nil 301 } 302 303 func readPrivateKey(pk string) (ssh.AuthMethod, error) { 304 // We parse the private key on our own first so that we can 305 // show a nicer error if the private key has a password. 306 block, _ := pem.Decode([]byte(pk)) 307 if block == nil { 308 return nil, errors.New("Failed to read ssh private key: no key found") 309 } 310 if block.Headers["Proc-Type"] == "4,ENCRYPTED" { 311 return nil, errors.New( 312 "Failed to read ssh private key: password protected keys are\n" + 313 "not supported. Please decrypt the key prior to use.") 314 } 315 316 signer, err := ssh.ParsePrivateKey([]byte(pk)) 317 if err != nil { 318 return nil, fmt.Errorf("Failed to parse ssh private key: %s", err) 319 } 320 321 return ssh.PublicKeys(signer), nil 322 } 323 324 func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) { 325 if connInfo.Agent != true { 326 // No agent configured 327 return nil, nil 328 } 329 330 agent, conn, err := sshagent.New() 331 if err != nil { 332 return nil, err 333 } 334 335 // connection close is handled over in Communicator 336 return &sshAgent{ 337 agent: agent, 338 conn: conn, 339 id: connInfo.AgentIdentity, 340 }, nil 341 342 } 343 344 // A tiny wrapper around an agent.Agent to expose the ability to close its 345 // associated connection on request. 346 type sshAgent struct { 347 agent agent.Agent 348 conn net.Conn 349 id string 350 } 351 352 func (a *sshAgent) Close() error { 353 if a.conn == nil { 354 return nil 355 } 356 357 return a.conn.Close() 358 } 359 360 // make an attempt to either read the identity file or find a corresponding 361 // public key file using the typical openssh naming convention. 362 // This returns the public key in wire format, or nil when a key is not found. 363 func findIDPublicKey(id string) []byte { 364 for _, d := range idKeyData(id) { 365 signer, err := ssh.ParsePrivateKey(d) 366 if err == nil { 367 log.Println("[DEBUG] parsed id private key") 368 pk := signer.PublicKey() 369 return pk.Marshal() 370 } 371 372 // try it as a publicKey 373 pk, err := ssh.ParsePublicKey(d) 374 if err == nil { 375 log.Println("[DEBUG] parsed id public key") 376 return pk.Marshal() 377 } 378 379 // finally try it as an authorized key 380 pk, _, _, _, err = ssh.ParseAuthorizedKey(d) 381 if err == nil { 382 log.Println("[DEBUG] parsed id authorized key") 383 return pk.Marshal() 384 } 385 } 386 387 return nil 388 } 389 390 // Try to read an id file using the id as the file path. Also read the .pub 391 // file if it exists, as the id file may be encrypted. Return only the file 392 // data read. We don't need to know what data came from which path, as we will 393 // try parsing each as a private key, a public key and an authorized key 394 // regardless. 395 func idKeyData(id string) [][]byte { 396 idPath, err := filepath.Abs(id) 397 if err != nil { 398 return nil 399 } 400 401 var fileData [][]byte 402 403 paths := []string{idPath} 404 405 if !strings.HasSuffix(idPath, ".pub") { 406 paths = append(paths, idPath+".pub") 407 } 408 409 for _, p := range paths { 410 d, err := ioutil.ReadFile(p) 411 if err != nil { 412 log.Printf("[DEBUG] error reading %q: %s", p, err) 413 continue 414 } 415 log.Printf("[DEBUG] found identity data at %q", p) 416 fileData = append(fileData, d) 417 } 418 419 return fileData 420 } 421 422 // sortSigners moves a signer with an agent comment field matching the 423 // agent_identity to the head of the list when attempting authentication. This 424 // helps when there are more keys loaded in an agent than the host will allow 425 // attempts. 426 func (s *sshAgent) sortSigners(signers []ssh.Signer) { 427 if s.id == "" || len(signers) < 2 { 428 return 429 } 430 431 // if we can locate the public key, either by extracting it from the id or 432 // locating the .pub file, then we can more easily determine an exact match 433 idPk := findIDPublicKey(s.id) 434 435 // if we have a signer with a connect field that matches the id, send that 436 // first, otherwise put close matches at the front of the list. 437 head := 0 438 for i := range signers { 439 pk := signers[i].PublicKey() 440 k, ok := pk.(*agent.Key) 441 if !ok { 442 continue 443 } 444 445 // check for an exact match first 446 if bytes.Equal(pk.Marshal(), idPk) || s.id == k.Comment { 447 signers[0], signers[i] = signers[i], signers[0] 448 break 449 } 450 451 // no exact match yet, move it to the front if it's close. The agent 452 // may have loaded as a full filepath, while the config refers to it by 453 // filename only. 454 if strings.HasSuffix(k.Comment, s.id) { 455 signers[head], signers[i] = signers[i], signers[head] 456 head++ 457 continue 458 } 459 } 460 461 ss := []string{} 462 for _, signer := range signers { 463 pk := signer.PublicKey() 464 k := pk.(*agent.Key) 465 ss = append(ss, k.Comment) 466 } 467 } 468 469 func (s *sshAgent) Signers() ([]ssh.Signer, error) { 470 signers, err := s.agent.Signers() 471 if err != nil { 472 return nil, err 473 } 474 475 s.sortSigners(signers) 476 return signers, nil 477 } 478 479 func (a *sshAgent) Auth() ssh.AuthMethod { 480 return ssh.PublicKeysCallback(a.Signers) 481 } 482 483 func (a *sshAgent) ForwardToAgent(client *ssh.Client) error { 484 return agent.ForwardToAgent(client, a.agent) 485 }