@ -44,6 +44,12 @@ type connectionInfo struct {
Timeout string
ScriptPath string ` mapstructure:"script_path" `
TimeoutVal time . Duration ` mapstructure:"-" `
BastionUser string ` mapstructure:"bastion_user" `
BastionPassword string ` mapstructure:"bastion_password" `
BastionKeyFile string ` mapstructure:"bastion_key_file" `
BastionHost string ` mapstructure:"bastion_host" `
BastionPort int ` mapstructure:"bastion_port" `
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
@ -86,6 +92,22 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo . TimeoutVal = DefaultTimeout
}
// Default all bastion config attrs to their non-bastion counterparts
if connInfo . BastionHost != "" {
if connInfo . BastionUser == "" {
connInfo . BastionUser = connInfo . User
}
if connInfo . BastionPassword == "" {
connInfo . BastionPassword = connInfo . Password
}
if connInfo . BastionKeyFile == "" {
connInfo . BastionKeyFile = connInfo . KeyFile
}
if connInfo . BastionPort == 0 {
connInfo . BastionPort = connInfo . Port
}
}
return connInfo , nil
}
@ -102,73 +124,152 @@ func safeDuration(dur string, defaultDur time.Duration) time.Duration {
// prepareSSHConfig is used to turn the *ConnectionInfo provided into a
// usable *SSHConfig for client initialization.
func prepareSSHConfig ( connInfo * connectionInfo ) ( * sshConfig , error ) {
var conn net . Conn
var err error
sshConf := & ssh . ClientConfig {
User : connInfo . User ,
sshAgent , err := connectToAgent ( connInfo )
if err != nil {
return nil , err
}
if connInfo . Agent {
sshAuthSock := os . Getenv ( "SSH_AUTH_SOCK" )
if sshAuthSock == "" {
return nil , fmt . Errorf ( "SSH Requested but SSH_AUTH_SOCK not-specified" )
}
sshConf , err := buildSSHClientConfig ( sshClientConfigOpts {
user : connInfo . User ,
keyFile : connInfo . KeyFile ,
password : connInfo . Password ,
sshAgent : sshAgent ,
} )
if err != nil {
return nil , err
}
conn , err = net . Dial ( "unix" , sshAuthSock )
var bastionConf * ssh . ClientConfig
if connInfo . BastionHost != "" {
bastionConf , err = buildSSHClientConfig ( sshClientConfigOpts {
user : connInfo . BastionUser ,
keyFile : connInfo . BastionKeyFile ,
password : connInfo . BastionPassword ,
sshAgent : sshAgent ,
} )
if err != nil {
return nil , fmt . Errorf ( "Error connecting to SSH_AUTH_SOCK: %v" , err )
}
// I need to close this but, later after all connections have been made
// defer conn.Close()
signers , err := agent . NewClient ( conn ) . Signers ( )
if err != nil {
return nil , fmt . Errorf ( "Error getting keys from ssh agent: %v" , err )
return nil , err
}
}
sshConf . Auth = append ( sshConf . Auth , ssh . PublicKeys ( signers ... ) )
host := fmt . Sprintf ( "%s:%d" , connInfo . Host , connInfo . Port )
connectFunc := ConnectFunc ( "tcp" , host )
if bastionConf != nil {
bastionHost := fmt . Sprintf ( "%s:%d" , connInfo . BastionHost , connInfo . BastionPort )
connectFunc = BastionConnectFunc ( "tcp" , bastionHost , bastionConf , "tcp" , host )
}
if connInfo . KeyFile != "" {
fullPath , err := homedir . Expand ( connInfo . KeyFile )
if err != nil {
return nil , fmt . Errorf ( "Failed to expand home directory: %v" , err )
}
key , err := ioutil . ReadFile ( fullPath )
if err != nil {
return nil , fmt . Errorf ( "Failed to read key file '%s': %v" , connInfo . KeyFile , err )
}
// We parse the private key on our own first so that we can
// show a nicer error if the private key has a password.
block , _ := pem . Decode ( key )
if block == nil {
return nil , fmt . Errorf (
"Failed to read key '%s': no key found" , connInfo . KeyFile )
}
if block . Headers [ "Proc-Type" ] == "4,ENCRYPTED" {
return nil , fmt . Errorf (
"Failed to read key '%s': password protected keys are\n" +
"not supported. Please decrypt the key prior to use." , connInfo . KeyFile )
}
config := & sshConfig {
config : sshConf ,
connection : connectFunc ,
sshAgent : sshAgent ,
}
return config , nil
}
type sshClientConfigOpts struct {
keyFile string
password string
sshAgent * sshAgent
user string
}
func buildSSHClientConfig ( opts sshClientConfigOpts ) ( * ssh . ClientConfig , error ) {
conf := & ssh . ClientConfig {
User : opts . user ,
}
if opts . sshAgent != nil {
conf . Auth = append ( conf . Auth , opts . sshAgent . Auth ( ) )
}
signer , err := ssh . ParsePrivateKey ( key )
if opts . keyFile != "" {
pubKeyAuth , err := readPublicKeyFromPath ( opts . keyFile )
if err != nil {
return nil , fmt . Errorf ( "Failed to parse key file '%s': %v" , connInfo . KeyFile , err )
return nil , err
}
conf . Auth = append ( conf . Auth , pubKeyAuth )
}
sshConf . Auth = append ( sshConf . Auth , ssh . PublicKeys ( signer ) )
if opts . password != "" {
conf . Auth = append ( conf . Auth , ssh . Password ( opts . password ) )
conf . Auth = append ( conf . Auth , ssh . KeyboardInteractive (
PasswordKeyboardInteractive ( opts . password ) ) )
}
if connInfo . Password != "" {
sshConf . Auth = append ( sshConf . Auth ,
ssh . Password ( connInfo . Password ) )
sshConf . Auth = append ( sshConf . Auth ,
ssh . KeyboardInteractive ( PasswordKeyboardInteractive ( connInfo . Password ) ) )
return conf , nil
}
func readPublicKeyFromPath ( path string ) ( ssh . AuthMethod , error ) {
fullPath , err := homedir . Expand ( path )
if err != nil {
return nil , fmt . Errorf ( "Failed to expand home directory: %s" , err )
}
host := fmt . Sprintf ( "%s:%d" , connInfo . Host , connInfo . Port )
config := & sshConfig {
config : sshConf ,
connection : ConnectFunc ( "tcp" , host ) ,
sshAgentConn : conn ,
key , err := ioutil . ReadFile ( fullPath )
if err != nil {
return nil , fmt . Errorf ( "Failed to read key file %q: %s" , path , err )
}
return config , nil
// We parse the private key on our own first so that we can
// show a nicer error if the private key has a password.
block , _ := pem . Decode ( key )
if block == nil {
return nil , fmt . Errorf ( "Failed to read key %q: no key found" , path )
}
if block . Headers [ "Proc-Type" ] == "4,ENCRYPTED" {
return nil , fmt . Errorf (
"Failed to read key %q: password protected keys are\n" +
"not supported. Please decrypt the key prior to use." , path )
}
signer , err := ssh . ParsePrivateKey ( key )
if err != nil {
return nil , fmt . Errorf ( "Failed to parse key file %q: %s" , path , err )
}
return ssh . PublicKeys ( signer ) , nil
}
func connectToAgent ( connInfo * connectionInfo ) ( * sshAgent , error ) {
if connInfo . Agent != true {
// No agent configured
return nil , nil
}
sshAuthSock := os . Getenv ( "SSH_AUTH_SOCK" )
if sshAuthSock == "" {
return nil , fmt . Errorf ( "SSH Requested but SSH_AUTH_SOCK not-specified" )
}
conn , err := net . Dial ( "unix" , sshAuthSock )
if err != nil {
return nil , fmt . Errorf ( "Error connecting to SSH_AUTH_SOCK: %v" , err )
}
// connection close is handled over in Communicator
return & sshAgent {
agent : agent . NewClient ( conn ) ,
conn : conn ,
} , nil
}
// A tiny wrapper around an agent.Agent to expose the ability to close its
// associated connection on request.
type sshAgent struct {
agent agent . Agent
conn net . Conn
}
func ( a * sshAgent ) Close ( ) error {
return a . conn . Close ( )
}
func ( a * sshAgent ) Auth ( ) ssh . AuthMethod {
return ssh . PublicKeysCallback ( a . agent . Signers )
}
func ( a * sshAgent ) ForwardToAgent ( client * ssh . Client ) error {
return agent . ForwardToAgent ( client , a . agent )
}