|
|
|
|
@ -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)
|
|
|
|
|
}
|
|
|
|
|
|