communicator/ssh: support for bastion SSH
This commit is contained in:
parent
b2609db395
commit
cbaaf0da52
|
@ -1,8 +1,11 @@
|
|||
package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// ConnectFunc is a convenience method for returning a function
|
||||
|
@ -23,3 +26,43 @@ func ConnectFunc(network, addr string) func() (net.Conn, error) {
|
|||
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 {
|
|||
SSHPty bool `mapstructure:"ssh_pty"`
|
||||
SSHTimeout time.Duration `mapstructure:"ssh_timeout"`
|
||||
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
|
||||
WinRMUser string `mapstructure:"winrm_username"`
|
||||
|
@ -77,6 +82,12 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
|
|||
c.SSHHandshakeAttempts = 10
|
||||
}
|
||||
|
||||
if c.SSHBastionHost != "" {
|
||||
if c.SSHBastionPort == 0 {
|
||||
c.SSHBastionPort = 22
|
||||
}
|
||||
}
|
||||
|
||||
// Validation
|
||||
var errs []error
|
||||
if c.SSHUsername == "" {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
|
@ -79,6 +81,24 @@ func (s *StepConnectSSH) Cleanup(multistep.StateBag) {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
var comm packer.Communicator
|
||||
|
@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru
|
|||
continue
|
||||
}
|
||||
|
||||
address := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// Attempt to connect to SSH port
|
||||
connFunc := ssh.ConnectFunc("tcp", address)
|
||||
var connFunc func() (net.Conn, error)
|
||||
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)
|
||||
}
|
||||
|
||||
nc, err := connFunc()
|
||||
if err != nil {
|
||||
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
|
|||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue