284 lines
7.3 KiB
Go
284 lines
7.3 KiB
Go
package ssh
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform/helper/pathorcontents"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/xanzy/ssh-agent"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
)
|
|
|
|
const (
|
|
// DefaultUser is used if there is no user given
|
|
DefaultUser = "root"
|
|
|
|
// DefaultPort is used if there is no port given
|
|
DefaultPort = 22
|
|
|
|
// DefaultScriptPath is used as the path to copy the file to
|
|
// for remote execution if not provided otherwise.
|
|
DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
|
|
|
|
// DefaultTimeout is used if there is no timeout given
|
|
DefaultTimeout = 5 * time.Minute
|
|
)
|
|
|
|
// connectionInfo is decoded from the ConnInfo of the resource. These are the
|
|
// only keys we look at. If a KeyFile is given, that is used instead
|
|
// of a password.
|
|
type connectionInfo struct {
|
|
User string
|
|
Password string
|
|
PrivateKey string `mapstructure:"private_key"`
|
|
Host string
|
|
Port int
|
|
Agent bool
|
|
Timeout string
|
|
ScriptPath string `mapstructure:"script_path"`
|
|
TimeoutVal time.Duration `mapstructure:"-"`
|
|
|
|
BastionUser string `mapstructure:"bastion_user"`
|
|
BastionPassword string `mapstructure:"bastion_password"`
|
|
BastionPrivateKey string `mapstructure:"bastion_private_key"`
|
|
BastionHost string `mapstructure:"bastion_host"`
|
|
BastionPort int `mapstructure:"bastion_port"`
|
|
|
|
// Deprecated
|
|
KeyFile string `mapstructure:"key_file"`
|
|
BastionKeyFile string `mapstructure:"bastion_key_file"`
|
|
}
|
|
|
|
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
|
|
// a ConnectionInfo struct
|
|
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
|
|
connInfo := &connectionInfo{}
|
|
decConf := &mapstructure.DecoderConfig{
|
|
WeaklyTypedInput: true,
|
|
Result: connInfo,
|
|
}
|
|
dec, err := mapstructure.NewDecoder(decConf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// To default Agent to true, we need to check the raw string, since the
|
|
// decoded boolean can't represent "absence of config".
|
|
//
|
|
// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
|
|
// shouldn't try.
|
|
if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
|
|
connInfo.Agent = true
|
|
}
|
|
|
|
if connInfo.User == "" {
|
|
connInfo.User = DefaultUser
|
|
}
|
|
if connInfo.Port == 0 {
|
|
connInfo.Port = DefaultPort
|
|
}
|
|
if connInfo.ScriptPath == "" {
|
|
connInfo.ScriptPath = DefaultScriptPath
|
|
}
|
|
if connInfo.Timeout != "" {
|
|
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
|
|
} else {
|
|
connInfo.TimeoutVal = DefaultTimeout
|
|
}
|
|
|
|
// Load deprecated fields; we can handle either path or contents in
|
|
// underlying implementation.
|
|
if connInfo.PrivateKey == "" && connInfo.KeyFile != "" {
|
|
connInfo.PrivateKey = connInfo.KeyFile
|
|
}
|
|
if connInfo.BastionPrivateKey == "" && connInfo.BastionKeyFile != "" {
|
|
connInfo.BastionPrivateKey = connInfo.BastionKeyFile
|
|
}
|
|
|
|
// 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.BastionPrivateKey == "" {
|
|
connInfo.BastionPrivateKey = connInfo.PrivateKey
|
|
}
|
|
if connInfo.BastionPort == 0 {
|
|
connInfo.BastionPort = connInfo.Port
|
|
}
|
|
}
|
|
|
|
return connInfo, nil
|
|
}
|
|
|
|
// safeDuration returns either the parsed duration or a default value
|
|
func safeDuration(dur string, defaultDur time.Duration) time.Duration {
|
|
d, err := time.ParseDuration(dur)
|
|
if err != nil {
|
|
log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
|
|
return defaultDur
|
|
}
|
|
return d
|
|
}
|
|
|
|
// prepareSSHConfig is used to turn the *ConnectionInfo provided into a
|
|
// usable *SSHConfig for client initialization.
|
|
func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
|
|
sshAgent, err := connectToAgent(connInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
|
|
user: connInfo.User,
|
|
privateKey: connInfo.PrivateKey,
|
|
password: connInfo.Password,
|
|
sshAgent: sshAgent,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var bastionConf *ssh.ClientConfig
|
|
if connInfo.BastionHost != "" {
|
|
bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
|
|
user: connInfo.BastionUser,
|
|
privateKey: connInfo.BastionPrivateKey,
|
|
password: connInfo.BastionPassword,
|
|
sshAgent: sshAgent,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
config := &sshConfig{
|
|
config: sshConf,
|
|
connection: connectFunc,
|
|
sshAgent: sshAgent,
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
type sshClientConfigOpts struct {
|
|
privateKey string
|
|
password string
|
|
sshAgent *sshAgent
|
|
user string
|
|
}
|
|
|
|
func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
|
|
conf := &ssh.ClientConfig{
|
|
User: opts.user,
|
|
}
|
|
|
|
if opts.privateKey != "" {
|
|
pubKeyAuth, err := readPrivateKey(opts.privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conf.Auth = append(conf.Auth, pubKeyAuth)
|
|
}
|
|
|
|
if opts.password != "" {
|
|
conf.Auth = append(conf.Auth, ssh.Password(opts.password))
|
|
conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
|
|
PasswordKeyboardInteractive(opts.password)))
|
|
}
|
|
|
|
if opts.sshAgent != nil {
|
|
conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
|
|
}
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
func readPrivateKey(pk string) (ssh.AuthMethod, error) {
|
|
key, _, err := pathorcontents.Read(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to read private key %q: %s", pk, 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([]byte(key))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("Failed to read key %q: no key found", pk)
|
|
}
|
|
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.", pk)
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey([]byte(key))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err)
|
|
}
|
|
|
|
return ssh.PublicKeys(signer), nil
|
|
}
|
|
|
|
func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
|
|
if connInfo.Agent != true {
|
|
// No agent configured
|
|
return nil, nil
|
|
}
|
|
|
|
agent, conn, err := sshagent.New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// connection close is handled over in Communicator
|
|
return &sshAgent{
|
|
agent: agent,
|
|
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 {
|
|
if a.conn == nil {
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|