github.com/inge4pres/terraform@v0.7.5-0.20160930053151-bd083f84f376/communicator/ssh/provisioner.go (about) 1 package ssh 2 3 import ( 4 "encoding/pem" 5 "fmt" 6 "log" 7 "net" 8 "os" 9 "time" 10 11 "github.com/hashicorp/terraform/communicator/shared" 12 "github.com/hashicorp/terraform/terraform" 13 "github.com/mitchellh/mapstructure" 14 "github.com/xanzy/ssh-agent" 15 "golang.org/x/crypto/ssh" 16 "golang.org/x/crypto/ssh/agent" 17 ) 18 19 const ( 20 // DefaultUser is used if there is no user given 21 DefaultUser = "root" 22 23 // DefaultPort is used if there is no port given 24 DefaultPort = 22 25 26 // DefaultScriptPath is used as the path to copy the file to 27 // for remote execution if not provided otherwise. 28 DefaultScriptPath = "/tmp/terraform_%RAND%.sh" 29 30 // DefaultTimeout is used if there is no timeout given 31 DefaultTimeout = 5 * time.Minute 32 ) 33 34 // connectionInfo is decoded from the ConnInfo of the resource. These are the 35 // only keys we look at. If a KeyFile is given, that is used instead 36 // of a password. 37 type connectionInfo struct { 38 User string 39 Password string 40 PrivateKey string `mapstructure:"private_key"` 41 Host string 42 Port int 43 Agent bool 44 Timeout string 45 ScriptPath string `mapstructure:"script_path"` 46 TimeoutVal time.Duration `mapstructure:"-"` 47 48 BastionUser string `mapstructure:"bastion_user"` 49 BastionPassword string `mapstructure:"bastion_password"` 50 BastionPrivateKey string `mapstructure:"bastion_private_key"` 51 BastionHost string `mapstructure:"bastion_host"` 52 BastionPort int `mapstructure:"bastion_port"` 53 54 // Deprecated 55 KeyFile string `mapstructure:"key_file"` 56 BastionKeyFile string `mapstructure:"bastion_key_file"` 57 } 58 59 // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into 60 // a ConnectionInfo struct 61 func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { 62 connInfo := &connectionInfo{} 63 decConf := &mapstructure.DecoderConfig{ 64 WeaklyTypedInput: true, 65 Result: connInfo, 66 } 67 dec, err := mapstructure.NewDecoder(decConf) 68 if err != nil { 69 return nil, err 70 } 71 if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil { 72 return nil, err 73 } 74 75 // To default Agent to true, we need to check the raw string, since the 76 // decoded boolean can't represent "absence of config". 77 // 78 // And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we 79 // shouldn't try. 80 if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" { 81 connInfo.Agent = true 82 } 83 84 if connInfo.User == "" { 85 connInfo.User = DefaultUser 86 } 87 88 // Format the host if needed. 89 // Needed for IPv6 support. 90 connInfo.Host = shared.IpFormat(connInfo.Host) 91 92 if connInfo.Port == 0 { 93 connInfo.Port = DefaultPort 94 } 95 if connInfo.ScriptPath == "" { 96 connInfo.ScriptPath = DefaultScriptPath 97 } 98 if connInfo.Timeout != "" { 99 connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout) 100 } else { 101 connInfo.TimeoutVal = DefaultTimeout 102 } 103 104 // Load deprecated fields; we can handle either path or contents in 105 // underlying implementation. 106 if connInfo.PrivateKey == "" && connInfo.KeyFile != "" { 107 connInfo.PrivateKey = connInfo.KeyFile 108 } 109 if connInfo.BastionPrivateKey == "" && connInfo.BastionKeyFile != "" { 110 connInfo.BastionPrivateKey = connInfo.BastionKeyFile 111 } 112 113 // Default all bastion config attrs to their non-bastion counterparts 114 if connInfo.BastionHost != "" { 115 // Format the bastion host if needed. 116 // Needed for IPv6 support. 117 connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost) 118 119 if connInfo.BastionUser == "" { 120 connInfo.BastionUser = connInfo.User 121 } 122 if connInfo.BastionPassword == "" { 123 connInfo.BastionPassword = connInfo.Password 124 } 125 if connInfo.BastionPrivateKey == "" { 126 connInfo.BastionPrivateKey = connInfo.PrivateKey 127 } 128 if connInfo.BastionPort == 0 { 129 connInfo.BastionPort = connInfo.Port 130 } 131 } 132 133 return connInfo, nil 134 } 135 136 // safeDuration returns either the parsed duration or a default value 137 func safeDuration(dur string, defaultDur time.Duration) time.Duration { 138 d, err := time.ParseDuration(dur) 139 if err != nil { 140 log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur) 141 return defaultDur 142 } 143 return d 144 } 145 146 // prepareSSHConfig is used to turn the *ConnectionInfo provided into a 147 // usable *SSHConfig for client initialization. 148 func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) { 149 sshAgent, err := connectToAgent(connInfo) 150 if err != nil { 151 return nil, err 152 } 153 154 sshConf, err := buildSSHClientConfig(sshClientConfigOpts{ 155 user: connInfo.User, 156 privateKey: connInfo.PrivateKey, 157 password: connInfo.Password, 158 sshAgent: sshAgent, 159 }) 160 if err != nil { 161 return nil, err 162 } 163 164 var bastionConf *ssh.ClientConfig 165 if connInfo.BastionHost != "" { 166 bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{ 167 user: connInfo.BastionUser, 168 privateKey: connInfo.BastionPrivateKey, 169 password: connInfo.BastionPassword, 170 sshAgent: sshAgent, 171 }) 172 if err != nil { 173 return nil, err 174 } 175 } 176 177 host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port) 178 connectFunc := ConnectFunc("tcp", host) 179 180 if bastionConf != nil { 181 bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort) 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 } 199 200 func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { 201 conf := &ssh.ClientConfig{ 202 User: opts.user, 203 } 204 205 if opts.privateKey != "" { 206 pubKeyAuth, err := readPrivateKey(opts.privateKey) 207 if err != nil { 208 return nil, err 209 } 210 conf.Auth = append(conf.Auth, pubKeyAuth) 211 } 212 213 if opts.password != "" { 214 conf.Auth = append(conf.Auth, ssh.Password(opts.password)) 215 conf.Auth = append(conf.Auth, ssh.KeyboardInteractive( 216 PasswordKeyboardInteractive(opts.password))) 217 } 218 219 if opts.sshAgent != nil { 220 conf.Auth = append(conf.Auth, opts.sshAgent.Auth()) 221 } 222 223 return conf, nil 224 } 225 226 func readPrivateKey(pk string) (ssh.AuthMethod, error) { 227 // We parse the private key on our own first so that we can 228 // show a nicer error if the private key has a password. 229 block, _ := pem.Decode([]byte(pk)) 230 if block == nil { 231 return nil, fmt.Errorf("Failed to read key %q: no key found", pk) 232 } 233 if block.Headers["Proc-Type"] == "4,ENCRYPTED" { 234 return nil, fmt.Errorf( 235 "Failed to read key %q: password protected keys are\n"+ 236 "not supported. Please decrypt the key prior to use.", pk) 237 } 238 239 signer, err := ssh.ParsePrivateKey([]byte(pk)) 240 if err != nil { 241 return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err) 242 } 243 244 return ssh.PublicKeys(signer), nil 245 } 246 247 func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) { 248 if connInfo.Agent != true { 249 // No agent configured 250 return nil, nil 251 } 252 253 agent, conn, err := sshagent.New() 254 if err != nil { 255 return nil, err 256 } 257 258 // connection close is handled over in Communicator 259 return &sshAgent{ 260 agent: agent, 261 conn: conn, 262 }, nil 263 264 } 265 266 // A tiny wrapper around an agent.Agent to expose the ability to close its 267 // associated connection on request. 268 type sshAgent struct { 269 agent agent.Agent 270 conn net.Conn 271 } 272 273 func (a *sshAgent) Close() error { 274 if a.conn == nil { 275 return nil 276 } 277 278 return a.conn.Close() 279 } 280 281 func (a *sshAgent) Auth() ssh.AuthMethod { 282 return ssh.PublicKeysCallback(a.agent.Signers) 283 } 284 285 func (a *sshAgent) ForwardToAgent(client *ssh.Client) error { 286 return agent.ForwardToAgent(client, a.agent) 287 }