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