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