Commit cbaaf0da authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

communicator/ssh: support for bastion SSH

parent b2609db3
package ssh package ssh
import ( import (
"fmt"
"net" "net"
"time" "time"
"golang.org/x/crypto/ssh"
) )
// ConnectFunc is a convenience method for returning a function // ConnectFunc is a convenience method for returning a function
...@@ -23,3 +26,43 @@ func ConnectFunc(network, addr string) func() (net.Conn, error) { ...@@ -23,3 +26,43 @@ func ConnectFunc(network, addr string) func() (net.Conn, error) {
return c, nil return c, nil
} }
} }
// BastionConnectFunc is a convenience method for returning a function
// that connects to a host over a bastion connection.
func BastionConnectFunc(
bProto string,
bAddr string,
bConf *ssh.ClientConfig,
proto string,
addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
// Connect to the bastion
bastion, err := ssh.Dial(bProto, bAddr, bConf)
if err != nil {
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
}
// Connect through to the end host
conn, err := bastion.Dial(proto, addr)
if err != nil {
bastion.Close()
return nil, err
}
// Wrap it up so we close both things properly
return &bastionConn{
Conn: conn,
Bastion: bastion,
}, nil
}
}
type bastionConn struct {
net.Conn
Bastion *ssh.Client
}
func (c *bastionConn) Close() error {
c.Conn.Close()
return c.Bastion.Close()
}
...@@ -23,6 +23,11 @@ type Config struct { ...@@ -23,6 +23,11 @@ type Config struct {
SSHPty bool `mapstructure:"ssh_pty"` SSHPty bool `mapstructure:"ssh_pty"`
SSHTimeout time.Duration `mapstructure:"ssh_timeout"` SSHTimeout time.Duration `mapstructure:"ssh_timeout"`
SSHHandshakeAttempts int `mapstructure:"ssh_handshake_attempts"` SSHHandshakeAttempts int `mapstructure:"ssh_handshake_attempts"`
SSHBastionHost string `mapstructure:"ssh_bastion_host"`
SSHBastionPort int `mapstructure:"ssh_bastion_port"`
SSHBastionUsername string `mapstructure:"ssh_bastion_username"`
SSHBastionPassword string `mapstructure:"ssh_bastion_password"`
SSHBastionPrivateKey string `mapstructure:"ssh_bastion_private_key_file"`
// WinRM // WinRM
WinRMUser string `mapstructure:"winrm_username"` WinRMUser string `mapstructure:"winrm_username"`
...@@ -77,6 +82,12 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { ...@@ -77,6 +82,12 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
c.SSHHandshakeAttempts = 10 c.SSHHandshakeAttempts = 10
} }
if c.SSHBastionHost != "" {
if c.SSHBastionPort == 0 {
c.SSHBastionPort = 22
}
}
// Validation // Validation
var errs []error var errs []error
if c.SSHUsername == "" { if c.SSHUsername == "" {
...@@ -93,6 +104,13 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { ...@@ -93,6 +104,13 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
} }
} }
if c.SSHBastionHost != "" {
if c.SSHBastionPassword == "" && c.SSHBastionPrivateKey == "" {
errs = append(errs, errors.New(
"ssh_bastion_password or ssh_bastion_private_key_file must be specified"))
}
}
return errs return errs
} }
......
...@@ -4,10 +4,12 @@ import ( ...@@ -4,10 +4,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"strings" "strings"
"time" "time"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
...@@ -79,6 +81,24 @@ func (s *StepConnectSSH) Cleanup(multistep.StateBag) { ...@@ -79,6 +81,24 @@ func (s *StepConnectSSH) Cleanup(multistep.StateBag) {
} }
func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) {
// Determine if we're using a bastion host, and if so, retrieve
// that configuration. This configuration doesn't change so we
// do this one before entering the retry loop.
var bProto, bAddr string
var bConf *gossh.ClientConfig
if s.Config.SSHBastionHost != "" {
// The protocol is hardcoded for now, but may be configurable one day
bProto = "tcp"
bAddr = fmt.Sprintf(
"%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort)
conf, err := sshBastionConfig(s.Config)
if err != nil {
return nil, fmt.Errorf("Error configuring bastion: %s", err)
}
bConf = conf
}
handshakeAttempts := 0 handshakeAttempts := 0
var comm packer.Communicator var comm packer.Communicator
...@@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru ...@@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
continue continue
} }
// Attempt to connect to SSH port
var connFunc func() (net.Conn, error)
address := fmt.Sprintf("%s:%d", host, port) address := fmt.Sprintf("%s:%d", host, port)
if bAddr != "" {
// We're using a bastion host, so use the bastion connfunc
connFunc = ssh.BastionConnectFunc(
bProto, bAddr, bConf, "tcp", address)
} else {
// No bastion host, connect directly
connFunc = ssh.ConnectFunc("tcp", address)
}
// Attempt to connect to SSH port
connFunc := ssh.ConnectFunc("tcp", address)
nc, err := connFunc() nc, err := connFunc()
if err != nil { if err != nil {
log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err)
...@@ -164,3 +192,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru ...@@ -164,3 +192,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
return comm, nil return comm, nil
} }
func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) {
auth := make([]gossh.AuthMethod, 0, 2)
if config.SSHBastionPassword != "" {
auth = append(auth,
gossh.Password(config.SSHBastionPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHBastionPassword)))
}
if config.SSHBastionPrivateKey != "" {
signer, err := commonssh.FileSigner(config.SSHBastionPrivateKey)
if err != nil {
return nil, err
}
auth = append(auth, gossh.PublicKeys(signer))
}
return &gossh.ClientConfig{
User: config.SSHBastionUsername,
Auth: auth,
}, nil
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment