github.com/ctrox/terraform@v0.11.12-beta1/communicator/ssh/provisioner.go (about)

     1  package ssh
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/pem"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"net"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/hashicorp/terraform/communicator/shared"
    16  	"github.com/hashicorp/terraform/terraform"
    17  	"github.com/mitchellh/mapstructure"
    18  	"github.com/xanzy/ssh-agent"
    19  	"golang.org/x/crypto/ssh"
    20  	"golang.org/x/crypto/ssh/agent"
    21  	"golang.org/x/crypto/ssh/knownhosts"
    22  )
    23  
    24  const (
    25  	// DefaultUser is used if there is no user given
    26  	DefaultUser = "root"
    27  
    28  	// DefaultPort is used if there is no port given
    29  	DefaultPort = 22
    30  
    31  	// DefaultScriptPath is used as the path to copy the file to
    32  	// for remote execution if not provided otherwise.
    33  	DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
    34  
    35  	// DefaultTimeout is used if there is no timeout given
    36  	DefaultTimeout = 5 * time.Minute
    37  )
    38  
    39  // connectionInfo is decoded from the ConnInfo of the resource. These are the
    40  // only keys we look at. If a PrivateKey is given, that is used instead
    41  // of a password.
    42  type connectionInfo struct {
    43  	User       string
    44  	Password   string
    45  	PrivateKey string `mapstructure:"private_key"`
    46  	Host       string
    47  	HostKey    string `mapstructure:"host_key"`
    48  	Port       int
    49  	Agent      bool
    50  	Timeout    string
    51  	ScriptPath string        `mapstructure:"script_path"`
    52  	TimeoutVal time.Duration `mapstructure:"-"`
    53  
    54  	BastionUser       string `mapstructure:"bastion_user"`
    55  	BastionPassword   string `mapstructure:"bastion_password"`
    56  	BastionPrivateKey string `mapstructure:"bastion_private_key"`
    57  	BastionHost       string `mapstructure:"bastion_host"`
    58  	BastionHostKey    string `mapstructure:"bastion_host_key"`
    59  	BastionPort       int    `mapstructure:"bastion_port"`
    60  
    61  	AgentIdentity string `mapstructure:"agent_identity"`
    62  }
    63  
    64  // parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
    65  // a ConnectionInfo struct
    66  func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
    67  	connInfo := &connectionInfo{}
    68  	decConf := &mapstructure.DecoderConfig{
    69  		WeaklyTypedInput: true,
    70  		Result:           connInfo,
    71  	}
    72  	dec, err := mapstructure.NewDecoder(decConf)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	// To default Agent to true, we need to check the raw string, since the
    81  	// decoded boolean can't represent "absence of config".
    82  	//
    83  	// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
    84  	// shouldn't try.
    85  	if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
    86  		connInfo.Agent = true
    87  	}
    88  
    89  	if connInfo.User == "" {
    90  		connInfo.User = DefaultUser
    91  	}
    92  
    93  	// Format the host if needed.
    94  	// Needed for IPv6 support.
    95  	connInfo.Host = shared.IpFormat(connInfo.Host)
    96  
    97  	if connInfo.Port == 0 {
    98  		connInfo.Port = DefaultPort
    99  	}
   100  	if connInfo.ScriptPath == "" {
   101  		connInfo.ScriptPath = DefaultScriptPath
   102  	}
   103  	if connInfo.Timeout != "" {
   104  		connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
   105  	} else {
   106  		connInfo.TimeoutVal = DefaultTimeout
   107  	}
   108  
   109  	// Default all bastion config attrs to their non-bastion counterparts
   110  	if connInfo.BastionHost != "" {
   111  		// Format the bastion host if needed.
   112  		// Needed for IPv6 support.
   113  		connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost)
   114  
   115  		if connInfo.BastionUser == "" {
   116  			connInfo.BastionUser = connInfo.User
   117  		}
   118  		if connInfo.BastionPassword == "" {
   119  			connInfo.BastionPassword = connInfo.Password
   120  		}
   121  		if connInfo.BastionPrivateKey == "" {
   122  			connInfo.BastionPrivateKey = connInfo.PrivateKey
   123  		}
   124  		if connInfo.BastionPort == 0 {
   125  			connInfo.BastionPort = connInfo.Port
   126  		}
   127  	}
   128  
   129  	return connInfo, nil
   130  }
   131  
   132  // safeDuration returns either the parsed duration or a default value
   133  func safeDuration(dur string, defaultDur time.Duration) time.Duration {
   134  	d, err := time.ParseDuration(dur)
   135  	if err != nil {
   136  		log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
   137  		return defaultDur
   138  	}
   139  	return d
   140  }
   141  
   142  // prepareSSHConfig is used to turn the *ConnectionInfo provided into a
   143  // usable *SSHConfig for client initialization.
   144  func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
   145  	sshAgent, err := connectToAgent(connInfo)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
   151  
   152  	sshConf, err := buildSSHClientConfig(sshClientConfigOpts{
   153  		user:       connInfo.User,
   154  		host:       host,
   155  		privateKey: connInfo.PrivateKey,
   156  		password:   connInfo.Password,
   157  		hostKey:    connInfo.HostKey,
   158  		sshAgent:   sshAgent,
   159  	})
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	connectFunc := ConnectFunc("tcp", host)
   165  
   166  	var bastionConf *ssh.ClientConfig
   167  	if connInfo.BastionHost != "" {
   168  		bastionHost := fmt.Sprintf("%s:%d", connInfo.BastionHost, connInfo.BastionPort)
   169  
   170  		bastionConf, err = buildSSHClientConfig(sshClientConfigOpts{
   171  			user:       connInfo.BastionUser,
   172  			host:       bastionHost,
   173  			privateKey: connInfo.BastionPrivateKey,
   174  			password:   connInfo.BastionPassword,
   175  			hostKey:    connInfo.HostKey,
   176  			sshAgent:   sshAgent,
   177  		})
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  
   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  	host       string
   199  	hostKey    string
   200  }
   201  
   202  func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) {
   203  	hkCallback := ssh.InsecureIgnoreHostKey()
   204  
   205  	if opts.hostKey != "" {
   206  		// The knownhosts package only takes paths to files, but terraform
   207  		// generally wants to handle config data in-memory. Rather than making
   208  		// the known_hosts file an exception, write out the data to a temporary
   209  		// file to create the HostKeyCallback.
   210  		tf, err := ioutil.TempFile("", "tf-known_hosts")
   211  		if err != nil {
   212  			return nil, fmt.Errorf("failed to create temp known_hosts file: %s", err)
   213  		}
   214  		defer tf.Close()
   215  		defer os.RemoveAll(tf.Name())
   216  
   217  		// we mark this as a CA as well, but the host key fallback will still
   218  		// use it as a direct match if the remote host doesn't return a
   219  		// certificate.
   220  		if _, err := tf.WriteString(fmt.Sprintf("@cert-authority %s %s\n", opts.host, opts.hostKey)); err != nil {
   221  			return nil, fmt.Errorf("failed to write temp known_hosts file: %s", err)
   222  		}
   223  		tf.Sync()
   224  
   225  		hkCallback, err = knownhosts.New(tf.Name())
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  	}
   230  
   231  	conf := &ssh.ClientConfig{
   232  		HostKeyCallback: hkCallback,
   233  		User:            opts.user,
   234  	}
   235  
   236  	if opts.privateKey != "" {
   237  		pubKeyAuth, err := readPrivateKey(opts.privateKey)
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		conf.Auth = append(conf.Auth, pubKeyAuth)
   242  	}
   243  
   244  	if opts.password != "" {
   245  		conf.Auth = append(conf.Auth, ssh.Password(opts.password))
   246  		conf.Auth = append(conf.Auth, ssh.KeyboardInteractive(
   247  			PasswordKeyboardInteractive(opts.password)))
   248  	}
   249  
   250  	if opts.sshAgent != nil {
   251  		conf.Auth = append(conf.Auth, opts.sshAgent.Auth())
   252  	}
   253  
   254  	return conf, nil
   255  }
   256  
   257  func readPrivateKey(pk string) (ssh.AuthMethod, error) {
   258  	// We parse the private key on our own first so that we can
   259  	// show a nicer error if the private key has a password.
   260  	block, _ := pem.Decode([]byte(pk))
   261  	if block == nil {
   262  		return nil, fmt.Errorf("Failed to read key %q: no key found", pk)
   263  	}
   264  	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
   265  		return nil, fmt.Errorf(
   266  			"Failed to read key %q: password protected keys are\n"+
   267  				"not supported. Please decrypt the key prior to use.", pk)
   268  	}
   269  
   270  	signer, err := ssh.ParsePrivateKey([]byte(pk))
   271  	if err != nil {
   272  		return nil, fmt.Errorf("Failed to parse key file %q: %s", pk, err)
   273  	}
   274  
   275  	return ssh.PublicKeys(signer), nil
   276  }
   277  
   278  func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
   279  	if connInfo.Agent != true {
   280  		// No agent configured
   281  		return nil, nil
   282  	}
   283  
   284  	agent, conn, err := sshagent.New()
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	// connection close is handled over in Communicator
   290  	return &sshAgent{
   291  		agent: agent,
   292  		conn:  conn,
   293  		id:    connInfo.AgentIdentity,
   294  	}, nil
   295  
   296  }
   297  
   298  // A tiny wrapper around an agent.Agent to expose the ability to close its
   299  // associated connection on request.
   300  type sshAgent struct {
   301  	agent agent.Agent
   302  	conn  net.Conn
   303  	id    string
   304  }
   305  
   306  func (a *sshAgent) Close() error {
   307  	if a.conn == nil {
   308  		return nil
   309  	}
   310  
   311  	return a.conn.Close()
   312  }
   313  
   314  // make an attempt to either read the identity file or find a corresponding
   315  // public key file using the typical openssh naming convention.
   316  // This returns the public key in wire format, or nil when a key is not found.
   317  func findIDPublicKey(id string) []byte {
   318  	for _, d := range idKeyData(id) {
   319  		signer, err := ssh.ParsePrivateKey(d)
   320  		if err == nil {
   321  			log.Println("[DEBUG] parsed id private key")
   322  			pk := signer.PublicKey()
   323  			return pk.Marshal()
   324  		}
   325  
   326  		// try it as a publicKey
   327  		pk, err := ssh.ParsePublicKey(d)
   328  		if err == nil {
   329  			log.Println("[DEBUG] parsed id public key")
   330  			return pk.Marshal()
   331  		}
   332  
   333  		// finally try it as an authorized key
   334  		pk, _, _, _, err = ssh.ParseAuthorizedKey(d)
   335  		if err == nil {
   336  			log.Println("[DEBUG] parsed id authorized key")
   337  			return pk.Marshal()
   338  		}
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  // Try to read an id file using the id as the file path. Also read the .pub
   345  // file if it exists, as the id file may be encrypted. Return only the file
   346  // data read. We don't need to know what data came from which path, as we will
   347  // try parsing each as a private key, a public key and an authorized key
   348  // regardless.
   349  func idKeyData(id string) [][]byte {
   350  	idPath, err := filepath.Abs(id)
   351  	if err != nil {
   352  		return nil
   353  	}
   354  
   355  	var fileData [][]byte
   356  
   357  	paths := []string{idPath}
   358  
   359  	if !strings.HasSuffix(idPath, ".pub") {
   360  		paths = append(paths, idPath+".pub")
   361  	}
   362  
   363  	for _, p := range paths {
   364  		d, err := ioutil.ReadFile(p)
   365  		if err != nil {
   366  			log.Printf("[DEBUG] error reading %q: %s", p, err)
   367  			continue
   368  		}
   369  		log.Printf("[DEBUG] found identity data at %q", p)
   370  		fileData = append(fileData, d)
   371  	}
   372  
   373  	return fileData
   374  }
   375  
   376  // sortSigners moves a signer with an agent comment field matching the
   377  // agent_identity to the head of the list when attempting authentication. This
   378  // helps when there are more keys loaded in an agent than the host will allow
   379  // attempts.
   380  func (s *sshAgent) sortSigners(signers []ssh.Signer) {
   381  	if s.id == "" || len(signers) < 2 {
   382  		return
   383  	}
   384  
   385  	// if we can locate the public key, either by extracting it from the id or
   386  	// locating the .pub file, then we can more easily determine an exact match
   387  	idPk := findIDPublicKey(s.id)
   388  
   389  	// if we have a signer with a connect field that matches the id, send that
   390  	// first, otherwise put close matches at the front of the list.
   391  	head := 0
   392  	for i := range signers {
   393  		pk := signers[i].PublicKey()
   394  		k, ok := pk.(*agent.Key)
   395  		if !ok {
   396  			continue
   397  		}
   398  
   399  		// check for an exact match first
   400  		if bytes.Equal(pk.Marshal(), idPk) || s.id == k.Comment {
   401  			signers[0], signers[i] = signers[i], signers[0]
   402  			break
   403  		}
   404  
   405  		// no exact match yet, move it to the front if it's close. The agent
   406  		// may have loaded as a full filepath, while the config refers to it by
   407  		// filename only.
   408  		if strings.HasSuffix(k.Comment, s.id) {
   409  			signers[head], signers[i] = signers[i], signers[head]
   410  			head++
   411  			continue
   412  		}
   413  	}
   414  
   415  	ss := []string{}
   416  	for _, signer := range signers {
   417  		pk := signer.PublicKey()
   418  		k := pk.(*agent.Key)
   419  		ss = append(ss, k.Comment)
   420  	}
   421  }
   422  
   423  func (s *sshAgent) Signers() ([]ssh.Signer, error) {
   424  	signers, err := s.agent.Signers()
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  
   429  	s.sortSigners(signers)
   430  	return signers, nil
   431  }
   432  
   433  func (a *sshAgent) Auth() ssh.AuthMethod {
   434  	return ssh.PublicKeysCallback(a.Signers)
   435  }
   436  
   437  func (a *sshAgent) ForwardToAgent(client *ssh.Client) error {
   438  	return agent.ForwardToAgent(client, a.agent)
   439  }