github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/communicator/ssh/provisioner.go (about)

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