diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index 2afd10700..07ffe83cf 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -125,11 +125,13 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) { " User: %s\n"+ " Password: %t\n"+ " Private key: %t\n"+ + " Certificate: %t\n"+ " SSH Agent: %t\n"+ " Checking Host Key: %t", c.connInfo.Host, c.connInfo.User, c.connInfo.Password != "", c.connInfo.PrivateKey != "", + c.connInfo.Certificate != "", c.connInfo.Agent, c.connInfo.HostKey != "", )) diff --git a/communicator/ssh/communicator_test.go b/communicator/ssh/communicator_test.go index 546d6f88c..91567d842 100644 --- a/communicator/ssh/communicator_test.go +++ b/communicator/ssh/communicator_test.go @@ -58,10 +58,10 @@ const testServerHostCert = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC1 const testCAPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrozyZIhdEvalCn+eSzHH94cO9ykiywA13ntWI7mJcHBwYTeCYWG8E9zGXyp2iDOjCGudM0Tdt8o0OofKChk9Z/qiUN0G8y1kmaXBlBM3qA5R9NPpvMYMNkYLfX6ivtZCnqrsbzaoqN2Oc/7H2StHzJWh/XCGu9otQZA6vdv1oSmAsZOjw/xIGaGQqDUaLq21J280PP1qSbdJHf76iSHE+TWe3YpqV946JWM5tCh0DykZ10VznvxYpUjzhr07IN3tVKxOXbPnnU7lX6IaLIWgfzLqwSyheeux05c3JLF9iF4sFu8ou4hwQz1iuUTU1jxgwZP0w/bkXgFFs0949lW81` -func newMockLineServer(t *testing.T, signer ssh.Signer) string { +func newMockLineServer(t *testing.T, signer ssh.Signer, pubKey string) string { serverConfig := &ssh.ServerConfig{ PasswordCallback: acceptUserPass("user", "pass"), - PublicKeyCallback: acceptPublicKey(testClientPublicKey), + PublicKeyCallback: acceptPublicKey(pubKey), } var err error @@ -119,7 +119,7 @@ func newMockLineServer(t *testing.T, signer ssh.Signer) string { } func TestNew_Invalid(t *testing.T) { - address := newMockLineServer(t, nil) + address := newMockLineServer(t, nil, testClientPublicKey) parts := strings.Split(address, ":") r := &terraform.InstanceState{ @@ -147,7 +147,7 @@ func TestNew_Invalid(t *testing.T) { } func TestStart(t *testing.T) { - address := newMockLineServer(t, nil) + address := newMockLineServer(t, nil, testClientPublicKey) parts := strings.Split(address, ":") r := &terraform.InstanceState{ @@ -180,7 +180,7 @@ func TestStart(t *testing.T) { } func TestLostConnection(t *testing.T) { - address := newMockLineServer(t, nil) + address := newMockLineServer(t, nil, testClientPublicKey) parts := strings.Split(address, ":") r := &terraform.InstanceState{ @@ -229,11 +229,11 @@ func TestHostKey(t *testing.T) { // get the server's public key signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey)) if err != nil { - panic("unable to parse private key: " + err.Error()) + t.Fatalf("unable to parse private key: %v", err) } pubKey := fmt.Sprintf("ssh-rsa %s", base64.StdEncoding.EncodeToString(signer.PublicKey().Marshal())) - address := newMockLineServer(t, nil) + address := newMockLineServer(t, nil, testClientPublicKey) host, p, _ := net.SplitHostPort(address) port, _ := strconv.Atoi(p) @@ -269,7 +269,7 @@ func TestHostKey(t *testing.T) { } // now check with the wrong HostKey - address = newMockLineServer(t, nil) + address = newMockLineServer(t, nil, testClientPublicKey) _, p, _ = net.SplitHostPort(address) port, _ = strconv.Atoi(p) @@ -308,7 +308,7 @@ func TestHostCert(t *testing.T) { t.Fatal(err) } - address := newMockLineServer(t, signer) + address := newMockLineServer(t, signer, testClientPublicKey) host, p, _ := net.SplitHostPort(address) port, _ := strconv.Atoi(p) @@ -344,7 +344,7 @@ func TestHostCert(t *testing.T) { } // now check with the wrong HostKey - address = newMockLineServer(t, signer) + address = newMockLineServer(t, signer, testClientPublicKey) _, p, _ = net.SplitHostPort(address) port, _ = strconv.Atoi(p) @@ -367,6 +367,105 @@ func TestHostCert(t *testing.T) { } } +const SERVER_PEM = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA8CkDr7uxCFt6lQUVwS8NyPO+fQNxORoGnMnN/XhVJZvpqyKR +Uji9R0d8D66bYxUUsabXjP2y4HTVzbZtnvXFZZshk0cOtJjjekpYJaLK2esPR/iX +wvSltNkrDQDPN/RmgEEMIevW8AgrPsqrnybFHxTpd7rEUHXBOe4nMNRIg3XHykB6 +jZk8q5bBPUe3I/f0DK5TJEBpTc6dO3P/j93u55VUqr39/SPRHnld2mCw+c8v6UOh +sssO/DIZFPScD3DYqsk2N+/nz9zXfcOTdWGhawgxuIo1DTokrNQbG3pDrLqcWgqj +13vqJFCmRA0O2CQIwJePd6+Np/XO3Uh/KL6FlQIDAQABAoIBAQCmvQMXNmvCDqk7 +30zsVDvw4fHGH+azK3Od1aqTqcEMHISOUbCtckFPxLzIsoSltRQqB1kuRVG07skm +Stsu+xny4lLcSwBVuLRuykEK2EyYIc/5Owo6y9pkhkaSf5ZfFes4bnD6+B/BhRpp +PRMMq0E+xCkX/G6iIi9mhgdlqm0x/vKtjzQeeshw9+gRcRLUpX+UeKFKXMXcDayx +qekr1bAaQKNBhTK+CbZjcqzG4f+BXVGRTZ9nsPAV+yTnWUCU0TghwPmtthHbebqa +9hlkum7qik/bQj/tjJ8/b0vTfHQSVxhtPG/ZV2Tn9ZuL/vrkYqeyMU8XkJ/uaEvH +WPyOcB4BAoGBAP5o5JSEtPog+U3JFrLNSRjz5ofZNVkJzice+0XyqlzJDHhX5tF8 +mriYQZLLXYhckBm4IdkhTn/dVbXNQTzyy2WVuO5nU8bkCMvGL9CGpW4YGqwGf7NX +e4H3emtRjLv8VZpUHe/RUUDhmYvMSt1qmXuskfpROuGfLhQBUd6A4J+BAoGBAPGp +UcMKjrxZ5qjYU6DLgS+xeca4Eu70HgdbSQbRo45WubXjyXvTRFij36DrpxJWf1D7 +lIsyBifoTra/lAuC1NQXGYWjTCdk2ey8Ll5qOgiXvE6lINHABr+U/Z90/g6LuML2 +VzaZbq/QLcT3yVsdyTogKckzCaKsCpusyHE1CXAVAoGAd6kMglKc8N0bhZukgnsN ++5+UeacPcY6sGTh4RWErAjNKGzx1A2lROKvcg9gFaULoQECcIw2IZ5nKW5VsLueg +BWrTrcaJ4A2XmYjhKnp6SvspaGoyHD90hx/Iw7t6r1yzQsB3yDmytwqldtyjBdvC +zynPC2azhDWjraMlR7tka4ECgYAxwvLiHa9sm3qCtCDsUFtmrb3srITBjaUNUL/F +1q8+JR+Sk7gudj9xnTT0VvINNaB71YIt83wPBagHu4VJpYQbtDH+MbUBu6OgOtO1 +f1w53rzY2OncJxV8p7pd9mJGLoE6LC2jQY7oRw7Vq0xcJdME1BCmrIrEY3a/vaF8 +pjYuTQKBgQCIOH23Xita8KmhH0NdlWxZfcQt1j3AnOcKe6UyN4BsF8hqS7eTA52s +WjG5X2IBl7gs1eMM1qkqR8npS9nwfO/pBmZPwjiZoilypXxWj+c+P3vwre2yija4 +bXgFVj4KFBwhr1+8KcobxC0SAPEouMvSkxzjjw+gnebozUtPlud9jA== +-----END RSA PRIVATE KEY----- +` +const CLIENT_CERT_SIGNED_BY_SERVER = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgbMDNUn4M2TtzrSH7MOT2QsvLzZWjehJ5TYrBOp9p+lwAAAADAQABAAABAQCyu57E7zIWRyEWuaiOiikOSZKFjbwLkpE9fboFfLLsNUJj4zw+5bZUJtzWK8roPjgL8s1oPncro5wuTtI2Nu4fkpeFK0Hb33o6Eyksuj4Om4+6Uemn1QEcb0bZqK8Zyg9Dg9deP7LeE0v78b5/jZafFgwxv+/sMhM0PRD34NCDYcYmkkHlvQtQWFAdbPXCgghObedZyYdoqZVuhTsiPMWtQS/cc9M4tv6mPOuQlhZt3R/Oh/kwUyu45oGRb5bhO4JicozFS3oeClpU+UMbgslkzApJqxZBWN7+PDFSZhKk2GslyeyP4sH3E30Z00yVi/lQYgmQsB+Hg6ClemNQMNu/AAAAAAAAAAAAAAACAAAABHVzZXIAAAAIAAAABHVzZXIAAAAAWzBjXAAAAAB/POfPAAAAAAAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA8CkDr7uxCFt6lQUVwS8NyPO+fQNxORoGnMnN/XhVJZvpqyKRUji9R0d8D66bYxUUsabXjP2y4HTVzbZtnvXFZZshk0cOtJjjekpYJaLK2esPR/iXwvSltNkrDQDPN/RmgEEMIevW8AgrPsqrnybFHxTpd7rEUHXBOe4nMNRIg3XHykB6jZk8q5bBPUe3I/f0DK5TJEBpTc6dO3P/j93u55VUqr39/SPRHnld2mCw+c8v6UOhsssO/DIZFPScD3DYqsk2N+/nz9zXfcOTdWGhawgxuIo1DTokrNQbG3pDrLqcWgqj13vqJFCmRA0O2CQIwJePd6+Np/XO3Uh/KL6FlQAAAQ8AAAAHc3NoLXJzYQAAAQC6sKEQHyl954BQn2BXuTgOB3NkENBxN7SD8ZaS8PNkDESytLjSIqrzoE6m7xuzprA+G23XRrCY/um3UvM7+7+zbwig2NIBbGbp3QFliQHegQKW6hTZP09jAQZk5jRrrEr/QT/s+gtHPmjxJK7XOQYxhInDKj+aJg62ExcwpQlP/0ATKNOIkdzTzzq916p0UOnnVaaPMKibh5Lv69GafIhKJRZSuuLN9fvs1G1RuUbxn/BNSeoRCr54L++Ztg09fJxunoyELs8mwgzCgB3pdZoUR2Z6ak05W4mvH3lkSz2BKUrlwxI6mterxhJy1GuN1K/zBG0gEMl2UTLajGK3qKM8 itbitloaner@MacBook-Pro-4.fios-router.home` +const CLIENT_PEM = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsruexO8yFkchFrmojoopDkmShY28C5KRPX26BXyy7DVCY+M8 +PuW2VCbc1ivK6D44C/LNaD53K6OcLk7SNjbuH5KXhStB2996OhMpLLo+DpuPulHp +p9UBHG9G2aivGcoPQ4PXXj+y3hNL+/G+f42WnxYMMb/v7DITND0Q9+DQg2HGJpJB +5b0LUFhQHWz1woIITm3nWcmHaKmVboU7IjzFrUEv3HPTOLb+pjzrkJYWbd0fzof5 +MFMruOaBkW+W4TuCYnKMxUt6HgpaVPlDG4LJZMwKSasWQVje/jwxUmYSpNhrJcns +j+LB9xN9GdNMlYv5UGIJkLAfh4OgpXpjUDDbvwIDAQABAoIBAEu2ctFVyk/pnbi0 +uRR4rl+hBvKQUeJNGj2ELvL4Ggs5nIAX2IOEZ7JKLC6FqpSrFq7pEd5g57aSvixX +s3DH4CN7w7fj1ShBCNPlHgIWewdRGpeA74vrDWdwNAEsFdDE6aZeCTOhpDGy1vNJ +OrtpzS5i9pN0jTvvEneEjtWSZIHiiVlN+0hsFaiwZ6KXON+sDccZPmnP6Fzwj5Rc +WS0dKSwnxnx0otWgwWFs8nr306nSeMsNmQkHsS9lz4DEVpp9owdzrX1JmbQvNYAV +ohmB3ET4JYFgerqPXJfed9poueGuWCP6MYhsjNeHN35QhofxdO5/0i3JlZfqwZei +tNq/0oECgYEA6SqjRqDiIp3ajwyB7Wf0cIQG/P6JZDyN1jl//htgniliIH5UP1Tm +uAMG5MincV6X9lOyXyh6Yofu5+NR0yt9SqbDZVJ3ZCxKTun7pxJvQFd7wl5bMkiJ +qVfS08k6gQHHDoO+eel+DtpIfWc+e3tvX0aihSU0GZEMqDXYkkphLGECgYEAxDxb ++JwJ3N5UEjjkuvFBpuJnmjIaN9HvQkTv3inlx1gLE4iWBZXXsu4aWF8MCUeAAZyP +42hQDSkCYX/A22tYCEn/jfrU6A+6rkWBTjdUlYLvlSkhosSnO+117WEItb5cUE95 +hF4UY7LNs1AsDkV4WE87f/EjpxSwUAjB2Lfd/B8CgYAJ/JiHsuZcozQ0Qk3iVDyF +ATKnbWOHFozgqw/PW27U92LLj32eRM2o/gAylmGNmoaZt1YBe2NaiwXxiqv7hnZU +VzYxRcn1UWxRWvY7Xq/DKrwTRCVVzwOObEOMbKcD1YaoGX50DEso6bKHJH/pnAzW +INlfKIvFuI+5OK0w/tyQoQKBgQCf/jpaOxaLfrV62eobRQJrByLDBGB97GsvU7di +IjTWz8DQH0d5rE7d8uWF8ZCFrEcAiV6DYZQK9smbJqbd/uoacAKtBro5rkFdPwwK +8m/DKqsdqRhkdgOHh7bjYH7Sdy8ax4Fi27WyB6FQtmgFBrz0+zyetsODwQlzZ4Bs +qpSRrwKBgQC0vWHrY5aGIdF+b8EpP0/SSLLALpMySHyWhDyxYcPqdhszYbjDcavv +xrrLXNUD2duBHKPVYE+7uVoDkpZXLUQ4x8argo/IwQM6Kh2ma1y83TYMT6XhL1+B +5UPcl6RXZBCkiU7nFIG6/0XKFqVWc3fU8e09X+iJwXIJ5Jatywtg+g== +-----END RSA PRIVATE KEY----- +` + +func TestCertificateBasedAuth(t *testing.T) { + signer, err := ssh.ParsePrivateKey([]byte(SERVER_PEM)) + if err != nil { + t.Fatalf("unable to parse private key: %v", err) + } + address := newMockLineServer(t, signer, CLIENT_CERT_SIGNED_BY_SERVER) + host, p, _ := net.SplitHostPort(address) + port, _ := strconv.Atoi(p) + + connInfo := &connectionInfo{ + User: "user", + Host: host, + PrivateKey: CLIENT_PEM, + Certificate: CLIENT_CERT_SIGNED_BY_SERVER, + Port: port, + Timeout: "30s", + } + + cfg, err := prepareSSHConfig(connInfo) + if err != nil { + t.Fatal(err) + } + + c := &Communicator{ + connInfo: connInfo, + config: cfg, + } + + var cmd remote.Cmd + stdout := new(bytes.Buffer) + cmd.Command = "echo foo" + cmd.Stdout = stdout + + if err := c.Start(&cmd); err != nil { + t.Fatal(err) + } + if err := c.Disconnect(); err != nil { + t.Fatal(err) + } +} + func TestAccUploadFile(t *testing.T) { // use the local ssh server and scp binary to check uploads if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" { @@ -572,11 +671,12 @@ func acceptUserPass(goodUser, goodPass string) func(ssh.ConnMetadata, []byte) (* } func acceptPublicKey(keystr string) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) { - goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr)) - if err != nil { - panic(fmt.Errorf("error parsing key: %s", err)) - } return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) { + goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr)) + if err != nil { + return nil, fmt.Errorf("error parsing key: %v", err) + } + if bytes.Equal(inkey.Marshal(), goodkey.Marshal()) { return nil, nil } diff --git a/communicator/ssh/provisioner.go b/communicator/ssh/provisioner.go index 2d8b5813a..21d73b276 100644 --- a/communicator/ssh/provisioner.go +++ b/communicator/ssh/provisioner.go @@ -41,16 +41,17 @@ const ( // only keys we look at. If a PrivateKey is given, that is used instead // of a password. type connectionInfo struct { - User string - Password string - PrivateKey string `mapstructure:"private_key"` - Host string - HostKey string `mapstructure:"host_key"` - Port int - Agent bool - Timeout string - ScriptPath string `mapstructure:"script_path"` - TimeoutVal time.Duration `mapstructure:"-"` + User string + Password string + PrivateKey string `mapstructure:"private_key"` + Certificate string `mapstructure:"certificate"` + Host string + HostKey string `mapstructure:"host_key"` + 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"` @@ -151,12 +152,13 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) { host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port) sshConf, err := buildSSHClientConfig(sshClientConfigOpts{ - user: connInfo.User, - host: host, - privateKey: connInfo.PrivateKey, - password: connInfo.Password, - hostKey: connInfo.HostKey, - sshAgent: sshAgent, + user: connInfo.User, + host: host, + privateKey: connInfo.PrivateKey, + password: connInfo.Password, + hostKey: connInfo.HostKey, + certificate: connInfo.Certificate, + sshAgent: sshAgent, }) if err != nil { return nil, err @@ -192,12 +194,13 @@ func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) { } type sshClientConfigOpts struct { - privateKey string - password string - sshAgent *sshAgent - user string - host string - hostKey string + privateKey string + password string + sshAgent *sshAgent + certificate string + user string + host string + hostKey string } func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { @@ -235,11 +238,23 @@ func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { } if opts.privateKey != "" { - pubKeyAuth, err := readPrivateKey(opts.privateKey) - if err != nil { - return nil, err + if opts.certificate != "" { + log.Println("using client certificate for authentication") + + certSigner, err := signCertWithPrivateKey(opts.privateKey, opts.certificate) + if err != nil { + return nil, err + } + conf.Auth = append(conf.Auth, certSigner) + } else { + log.Println("using private key for authentication") + + pubKeyAuth, err := readPrivateKey(opts.privateKey) + if err != nil { + return nil, err + } + conf.Auth = append(conf.Auth, pubKeyAuth) } - conf.Auth = append(conf.Auth, pubKeyAuth) } if opts.password != "" { @@ -255,6 +270,31 @@ func buildSSHClientConfig(opts sshClientConfigOpts) (*ssh.ClientConfig, error) { return conf, nil } +// Create a Cert Signer and return ssh.AuthMethod +func signCertWithPrivateKey(pk string, certificate string) (ssh.AuthMethod, error) { + rawPk, err := ssh.ParseRawPrivateKey([]byte(pk)) + if err != nil { + return nil, fmt.Errorf("failed to parse private key %q: %s", pk, err) + } + + pcert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certificate)) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate %q: %s", certificate, err) + } + + usigner, err := ssh.NewSignerFromKey(rawPk) + if err != nil { + return nil, fmt.Errorf("failed to create signer from raw private key %q: %s", rawPk, err) + } + + ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner) + if err != nil { + return nil, fmt.Errorf("failed to create cert signer %q: %s", usigner, err) + } + + return ssh.PublicKeys(ucertSigner), nil +} + func readPrivateKey(pk string) (ssh.AuthMethod, error) { // We parse the private key on our own first so that we can // show a nicer error if the private key has a password. diff --git a/communicator/ssh/provisioner_test.go b/communicator/ssh/provisioner_test.go index 601d53e54..9eb5af7c8 100644 --- a/communicator/ssh/provisioner_test.go +++ b/communicator/ssh/provisioner_test.go @@ -14,6 +14,7 @@ func TestProvisioner_connInfo(t *testing.T) { "user": "root", "password": "supersecret", "private_key": "someprivatekeycontents", + "certificate": "somecertificate", "host": "127.0.0.1", "port": "22", "timeout": "30s", @@ -37,6 +38,9 @@ func TestProvisioner_connInfo(t *testing.T) { if conf.PrivateKey != "someprivatekeycontents" { t.Fatalf("bad: %v", conf) } + if conf.Certificate != "somecertificate" { + t.Fatalf("bad: %v", conf) + } if conf.Host != "127.0.0.1" { t.Fatalf("bad: %v", conf) } @@ -74,6 +78,7 @@ func TestProvisioner_connInfoIpv6(t *testing.T) { "user": "root", "password": "supersecret", "private_key": "someprivatekeycontents", + "certificate": "somecertificate", "host": "::1", "port": "22", "timeout": "30s", @@ -101,14 +106,13 @@ func TestProvisioner_connInfoHostname(t *testing.T) { r := &terraform.InstanceState{ Ephemeral: terraform.EphemeralState{ ConnInfo: map[string]string{ - "type": "ssh", - "user": "root", - "password": "supersecret", - "private_key": "someprivatekeycontents", - "host": "example.com", - "port": "22", - "timeout": "30s", - + "type": "ssh", + "user": "root", + "password": "supersecret", + "private_key": "someprivatekeycontents", + "host": "example.com", + "port": "22", + "timeout": "30s", "bastion_host": "example.com", }, }, diff --git a/terraform/eval_validate.go b/terraform/eval_validate.go index aaae21e8d..5fe28b39a 100644 --- a/terraform/eval_validate.go +++ b/terraform/eval_validate.go @@ -252,6 +252,10 @@ var connectionBlockSupersetSchema = &configschema.Block{ Type: cty.String, Optional: true, }, + "certificate": { + Type: cty.String, + Optional: true, + }, "host_key": { Type: cty.String, Optional: true, diff --git a/website/docs/provisioners/connection.html.markdown b/website/docs/provisioners/connection.html.markdown index d983a8545..ac59c5719 100644 --- a/website/docs/provisioners/connection.html.markdown +++ b/website/docs/provisioners/connection.html.markdown @@ -77,6 +77,10 @@ provisioner "file" { [the `file` function](/docs/configuration/functions/file.html). This takes preference over the password if provided. +* `certificate` - The contents of a signed CA Certificate. The certificate argument must be + used in conjunction with a `private_key`. These can + be loaded from a file on disk using the [the `file` function](/docs/configuration/functions/file.html). + * `agent` - Set to `false` to disable using `ssh-agent` to authenticate. On Windows the only supported SSH authentication agent is [Pageant](http://the.earth.li/~sgtatham/putty/0.66/htmldoc/Chapter9.html#pageant).