diff --git a/builder/virtualbox/common/step_ssh_key_pair.go b/builder/virtualbox/common/step_ssh_key_pair.go new file mode 100644 index 000000000..929bc9526 --- /dev/null +++ b/builder/virtualbox/common/step_ssh_key_pair.go @@ -0,0 +1,107 @@ +package common + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/helper/ssh" + "github.com/hashicorp/packer/packer" +) + +// StepSshKeyPair executes the business logic for setting the SSH key pair in +// the specified communicator.Config. +type StepSshKeyPair struct { + Debug bool + DebugKeyPath string + Comm *communicator.Config +} + +func (s *StepSshKeyPair) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + if s.Comm.SSHPassword != "" { + return multistep.ActionContinue + } + + ui := state.Get("ui").(packer.Ui) + + if s.Comm.SSHPrivateKeyFile != "" { + ui.Say("Using existing SSH private key for the communicator...") + privateKeyBytes, err := s.Comm.ReadSSHPrivateKeyFile() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{ + RawPrivateKeyPemBlock: privateKeyBytes, + Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), + }) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + s.Comm.SSHPrivateKey = privateKeyBytes + s.Comm.SSHKeyPairName = kp.Comment + s.Comm.SSHTemporaryKeyPairName = kp.Comment + s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + + return multistep.ActionContinue + } + + if s.Comm.SSHAgentAuth { + ui.Say("Using local SSH Agent to authenticate connections for the communicator...") + return multistep.ActionContinue + } + + ui.Say("Creating ephemeral key pair for SSH communicator...") + + kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{ + Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), + }) + if err != nil { + state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) + return multistep.ActionHalt + } + + s.Comm.SSHKeyPairName = kp.Comment + s.Comm.SSHTemporaryKeyPairName = kp.Comment + s.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock + s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + s.Comm.SSHClearAuthorizedKeys = true + + ui.Say("Created ephemeral SSH key pair for communicator") + + // If we're in debug mode, output the private key to the working + // directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving communicator private key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.OpenFile(s.DebugKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write(kp.PrivateKeyPemBlock); err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepSshKeyPair) Cleanup(state multistep.StateBag) { + if s.Debug { + if err := os.Remove(s.DebugKeyPath); err != nil { + ui := state.Get("ui").(packer.Ui) + ui.Error(fmt.Sprintf( + "Error removing debug key '%s': %s", s.DebugKeyPath, err)) + } + } +} diff --git a/builder/virtualbox/common/step_type_boot_command.go b/builder/virtualbox/common/step_type_boot_command.go index 859e425ff..fb596cc82 100644 --- a/builder/virtualbox/common/step_type_boot_command.go +++ b/builder/virtualbox/common/step_type_boot_command.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" @@ -14,10 +15,20 @@ import ( const KeyLeftShift uint32 = 0xFFE1 +// TODO: Should this be made available for other builders? +// It is copy pasted in the VMWare builder as well. type bootCommandTemplateData struct { - HTTPIP string + // HTTPIP is the HTTP server's IP address. + HTTPIP string + + // HTTPPort is the HTTP server port. HTTPPort uint - Name string + + // Name is the VM's name. + Name string + + // SSHPublicKey is the SSH public key in OpenSSH authorized_keys format. + SSHPublicKey string } type StepTypeBootCommand struct { @@ -26,6 +37,7 @@ type StepTypeBootCommand struct { VMName string Ctx interpolate.Context GroupInterval time.Duration + Comm *communicator.Config } func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { @@ -54,9 +66,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) hostIP := "10.0.2.2" common.SetHTTPIP(hostIP) s.Ctx.Data = &bootCommandTemplateData{ - hostIP, - httpPort, - s.VMName, + HTTPIP: hostIP, + HTTPPort: httpPort, + Name: s.VMName, + SSHPublicKey: string(s.Comm.SSHPublicKey), } sendCodes := func(codes []string) error { diff --git a/builder/virtualbox/iso/builder.go b/builder/virtualbox/iso/builder.go index dce65c4df..6b216baf1 100644 --- a/builder/virtualbox/iso/builder.go +++ b/builder/virtualbox/iso/builder.go @@ -227,6 +227,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe HTTPPortMin: b.config.HTTPPortMin, HTTPPortMax: b.config.HTTPPortMax, }, + &vboxcommon.StepSshKeyPair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName), + Comm: &b.config.Comm, + }, new(vboxcommon.StepSuppressMessages), new(stepCreateVM), new(stepCreateDisk), @@ -260,6 +265,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe VMName: b.config.VMName, Ctx: b.config.ctx, GroupInterval: b.config.BootConfig.BootGroupInterval, + Comm: &b.config.Comm, }, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, diff --git a/builder/virtualbox/ovf/builder.go b/builder/virtualbox/ovf/builder.go index 3a8b3f901..74cce0b02 100644 --- a/builder/virtualbox/ovf/builder.go +++ b/builder/virtualbox/ovf/builder.go @@ -64,6 +64,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe HTTPPortMin: b.config.HTTPPortMin, HTTPPortMax: b.config.HTTPPortMax, }, + &vboxcommon.StepSshKeyPair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName), + Comm: &b.config.Comm, + }, &vboxcommon.StepDownloadGuestAdditions{ GuestAdditionsMode: b.config.GuestAdditionsMode, GuestAdditionsURL: b.config.GuestAdditionsURL, @@ -112,6 +117,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe VMName: b.config.VMName, Ctx: b.config.ctx, GroupInterval: b.config.BootConfig.BootGroupInterval, + Comm: &b.config.Comm, }, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, diff --git a/common/step_cleanup_temp_keys.go b/common/step_cleanup_temp_keys.go index 1d5fec1e5..56ca700aa 100644 --- a/common/step_cleanup_temp_keys.go +++ b/common/step_cleanup_temp_keys.go @@ -38,13 +38,29 @@ func (s *StepCleanupTempKeys) Run(_ context.Context, state multistep.StateBag) m ui.Say("Trying to remove ephemeral keys from authorized_keys files") - cmd.Command = fmt.Sprintf("sed -i.bak '/ssh-rsa.*%s$/d' ~/.ssh/authorized_keys; rm ~/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName) + // Per the OpenSSH manual (https://man.openbsd.org/sshd.8), a typical + // line in the 'authorized_keys' file contains several fields that + // are delimited by spaces. Here is an (abbreviated) example of a line: + // ssh-rsa AAAAB3Nza...LiPk== user@example.net + // + // In the above example, 'ssh-rsa' is the key pair type, + // 'AAAAB3Nza...LiPk==' is the base64 encoded public key, + // and 'user@example.net' is a comment (in this case, describing + // who the key belongs to). + // + // In the following 'sed' calls, the comment field will be equal to + // the value of communicator.Config.SSHTemporaryKeyPairName. + // We can remove an authorized public key using 'sed' by looking + // for a line ending in ' packer-key-pair-comment' (note the + // leading space). + // + // TODO: Why create a backup file if you are going to remove it? + cmd.Command = fmt.Sprintf("sed -i.bak '/ %s$/d' ~/.ssh/authorized_keys; rm ~/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName) if err := cmd.StartWithUi(comm, ui); err != nil { log.Printf("Error cleaning up ~/.ssh/authorized_keys; please clean up keys manually: %s", err) } cmd = new(packer.RemoteCmd) - cmd.Command = fmt.Sprintf("sudo sed -i.bak '/ssh-rsa.*%s$/d' /root/.ssh/authorized_keys; sudo rm /root/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName) - + cmd.Command = fmt.Sprintf("sudo sed -i.bak '/ %s$/d' /root/.ssh/authorized_keys; sudo rm /root/.ssh/authorized_keys.bak", s.Comm.SSHTemporaryKeyPairName) if err := cmd.StartWithUi(comm, ui); err != nil { log.Printf("Error cleaning up /root/.ssh/authorized_keys; please clean up keys manually: %s", err) } diff --git a/helper/ssh/key_pair.go b/helper/ssh/key_pair.go new file mode 100644 index 000000000..fd052dc2e --- /dev/null +++ b/helper/ssh/key_pair.go @@ -0,0 +1,258 @@ +package ssh + +import ( + "bytes" + "crypto" + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + + gossh "golang.org/x/crypto/ssh" +) + +const ( + // defaultRsaBits is the default bits of entropy for a new RSA + // key pair. That's a lot of bits. + defaultRsaBits = 4096 + + // Markers for various SSH key pair types. + Default KeyPairType = "" + Rsa KeyPairType = "RSA" + Ecdsa KeyPairType = "ECDSA" + Dsa KeyPairType = "DSA" + Ed25519 KeyPairType = "ED25519" +) + +// KeyPairType represents different types of SSH key pairs. +// See the 'const' block for details. +type KeyPairType string + +func (o KeyPairType) String() string { + return string(o) +} + +// KeyPair represents an SSH key pair. +type KeyPair struct { + // PrivateKeyPemBlock represents the key pair's private key in + // ASN.1 Distinguished Encoding Rules (DER) format in a + // Privacy-Enhanced Mail (PEM) block. + PrivateKeyPemBlock []byte + + // PublicKeyAuthorizedKeysLine represents the key pair's public key + // as a line in OpenSSH authorized_keys. + PublicKeyAuthorizedKeysLine []byte + + // Comment is the key pair's comment. This is typically used + // to identify the key pair's owner in the SSH user's + // 'authorized_keys' file. + Comment string +} + +// KeyPairFromPrivateKey returns a KeyPair loaded from an existing private key. +// +// Supported key pair types include: +// - DSA +// - ECDSA +// - ED25519 +// - RSA +func KeyPairFromPrivateKey(config FromPrivateKeyConfig) (KeyPair, error) { + privateKey, err := gossh.ParseRawPrivateKey(config.RawPrivateKeyPemBlock) + if err != nil { + return KeyPair{}, err + } + + switch pk := privateKey.(type) { + case crypto.Signer: + // crypto.Signer is implemented by ecdsa.PrivateKey, + // ed25519.PrivateKey, and rsa.PrivateKey - separate cases + // for each PrivateKey type would be redundant. + publicKey, err := gossh.NewPublicKey(pk.Public()) + if err != nil { + return KeyPair{}, err + } + return KeyPair{ + PrivateKeyPemBlock: config.RawPrivateKeyPemBlock, + PublicKeyAuthorizedKeysLine: authorizedKeysLine(publicKey, config.Comment), + }, nil + case *dsa.PrivateKey: + publicKey, err := gossh.NewPublicKey(&pk.PublicKey) + if err != nil { + return KeyPair{}, err + } + return KeyPair{ + PrivateKeyPemBlock: config.RawPrivateKeyPemBlock, + PublicKeyAuthorizedKeysLine: authorizedKeysLine(publicKey, config.Comment), + }, nil + } + + return KeyPair{}, fmt.Errorf("Cannot parse existing SSH key pair - unknown key pair type") +} + +// FromPrivateKeyConfig describes how an SSH key pair should be loaded from an +// existing private key. +type FromPrivateKeyConfig struct { + // RawPrivateKeyPemBlock is the raw private key that the key pair + // should be loaded from. + RawPrivateKeyPemBlock []byte + + // Comment is the key pair's comment. This is typically used + // to identify the key pair's owner in the SSH user's + // 'authorized_keys' file. + Comment string +} + +// NewKeyPair generates a new SSH key pair using the specified +// CreateKeyPairConfig. +func NewKeyPair(config CreateKeyPairConfig) (KeyPair, error) { + if config.Type == Default { + config.Type = Ecdsa + } + + switch config.Type { + case Ecdsa: + return newEcdsaKeyPair(config) + case Rsa: + return newRsaKeyPair(config) + } + + return KeyPair{}, fmt.Errorf("Unable to generate new key pair, type %s is not supported", + config.Type.String()) +} + +// newEcdsaKeyPair returns a new ECDSA SSH key pair. +func newEcdsaKeyPair(config CreateKeyPairConfig) (KeyPair, error) { + var curve elliptic.Curve + + switch config.Bits { + case 0: + config.Bits = 521 + fallthrough + case 521: + curve = elliptic.P521() + case 384: + curve = elliptic.P384() + case 256: + curve = elliptic.P256() + case 224: + // Not supported by "golang.org/x/crypto/ssh". + return KeyPair{}, fmt.Errorf("golang.org/x/crypto/ssh does not support %d bits", config.Bits) + default: + return KeyPair{}, fmt.Errorf("crypto/elliptic does not support %d bits", config.Bits) + } + + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return KeyPair{}, err + } + + sshPublicKey, err := gossh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return KeyPair{}, err + } + + privateRaw, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return KeyPair{}, err + } + + privatePem, err := rawPemBlock(&pem.Block{ + Type: "EC PRIVATE KEY", + Headers: nil, + Bytes: privateRaw, + }) + if err != nil { + return KeyPair{}, err + } + + return KeyPair{ + PrivateKeyPemBlock: privatePem, + PublicKeyAuthorizedKeysLine: authorizedKeysLine(sshPublicKey, config.Comment), + Comment: config.Comment, + }, nil +} + +// newRsaKeyPair returns a new RSA SSH key pair. +func newRsaKeyPair(config CreateKeyPairConfig) (KeyPair, error) { + if config.Bits == 0 { + config.Bits = defaultRsaBits + } + + privateKey, err := rsa.GenerateKey(rand.Reader, config.Bits) + if err != nil { + return KeyPair{}, err + } + + sshPublicKey, err := gossh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return KeyPair{}, err + } + + privatePemBlock, err := rawPemBlock(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + if err != nil { + return KeyPair{}, err + } + + return KeyPair{ + PrivateKeyPemBlock: privatePemBlock, + PublicKeyAuthorizedKeysLine: authorizedKeysLine(sshPublicKey, config.Comment), + Comment: config.Comment, + }, nil +} + +// CreateKeyPairConfig describes how an SSH key pair should be created. +type CreateKeyPairConfig struct { + // Type describes the key pair's type. + Type KeyPairType + + // Bits represents the key pair's bits of entropy. E.g., 4096 for + // a 4096 bit RSA key pair, or 521 for a ECDSA key pair with a + // 521-bit curve. + Bits int + + // Comment is the resulting key pair's comment. This is typically + // used to identify the key pair's owner in the SSH user's + // 'authorized_keys' file. + Comment string +} + +// rawPemBlock encodes a pem.Block to a slice of bytes. +func rawPemBlock(block *pem.Block) ([]byte, error) { + buffer := bytes.NewBuffer(nil) + + err := pem.Encode(buffer, block) + if err != nil { + return []byte{}, err + } + + return buffer.Bytes(), nil +} + +// authorizedKeysLine serializes key for inclusion in an OpenSSH +// authorized_keys file. The return value ends without newline so +// a comment can be appended to the end. +func authorizedKeysLine(key gossh.PublicKey, comment string) []byte { + marshaledPublicKey := gossh.MarshalAuthorizedKey(key) + + // Remove the mandatory unix new line. Awful, but the go + // ssh library automatically appends a unix new line. + // We remove it so a key comment can be safely appended to the + // end of the string. + marshaledPublicKey = bytes.TrimSpace(marshaledPublicKey) + + if len(strings.TrimSpace(comment)) > 0 { + marshaledPublicKey = append(marshaledPublicKey, ' ') + marshaledPublicKey = append(marshaledPublicKey, comment...) + } + + return marshaledPublicKey +} diff --git a/helper/ssh/key_pair_test.go b/helper/ssh/key_pair_test.go new file mode 100644 index 000000000..3c98c84e8 --- /dev/null +++ b/helper/ssh/key_pair_test.go @@ -0,0 +1,449 @@ +package ssh + +import ( + "bytes" + "crypto/dsa" + "crypto/ecdsa" + "crypto/rsa" + "fmt" + "testing" + + "github.com/hashicorp/packer/common/uuid" + "golang.org/x/crypto/ed25519" + gossh "golang.org/x/crypto/ssh" +) + +const ( + pemRsa1024 = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDJEMFPpTBiWNDb3qEIPTSeEnIP8FZdBpG8njOrclcMoQQNhzZ+ +4uz37tqtHMp36Z7LB4/+85NN6epNXO+ekyZIHswiyBcJC2sT3KuH7nG1BESOooPY +DfeCSM+CJT9GDIhy9nUXSsJjrceEyh/B5DjEtIbS0XfcRelrNTJodCmPJwIDAQAB +AoGAK66GMOV0c4lUJtBhL8cMTWM4gJn4SVGKC+5az16R5t58YOwFPN/UF7E+tOlS +W2bX5sgH0p3cXMr66j/Mlyjk4deLg7trDavulIP93MyVO2SUJ0cstQ0ZmRz2oGwx +Gow+hD75Cet7uvepdmG4DKHJe8D/I72rtP1WKuZyd0vP6WECQQDua6wWlyEdIimx +XoGWUvmywACWPnQmBnyHG7x5hxMjijQoQZu60zRxSU9I5q08BerTsvbTc+xLnDVv +mFzlcjT/AkEA1+P7lcvViZeNKoDB1Qt+VV+pkcqL5aoRwdnLA51SyFJ9tXkxeZwA +LOof3xtoRGhCld7ixi3kF5aZsafAJOZd2QJAH8rFyMFgTgU3MAqdFxF7cGV/7ojn +bgahZlbBfCcR20Rbjh6piHEPZifTZbI02XMkjBQqK6oikTaEPZxAjuv6uwJANczu +yWm+kUdfOpRTuY/fr87jJx3etyEmw7ROz1vJYXqNMUg+eBvUP10pDCR8W2/QCCE/ +Sjvtd6NkMc2oKInwIQJAFZ1xJte0EaQsXaCIoZwHrQJbK1dd5l1xTAzz51voAcKH +2K23xgx4I+/eam2enjFa7wXLZFoW0xg/51xsaIjnrA== +-----END RSA PRIVATE KEY----- +` + pemRsa2048 = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA/ZPazeRmBapF01gzHXtJGpu0S936xHY+pOrIyIk6lEE06paf +q5gh6BCuiN/60Keed5Nz+Es4dPGc73mql9pd7N0HOoEc1IQjZzJVqWOy3E55oWbz +rXr1qbmMjw8bGHalZsVBov1UhyB6f2bKi88fGkThJi9HZ+Dc3Jr87eW+whS4D0bI +JJe5dkY0VhDqB0YVEk299TxlAiDkeXD1EcMZrD/yHsusapwlXL2WHWmCgbPpbeYW +YJhD1bScChYmf41iiInBwFymG7kz4bPsup7wCBXpcLJplY1iuXdtVVujNLDbJwlb +Xi2oBm3WizPjYcUthvMlqOieuy6Z4KzyJd7EnQIDAQABAoIBAByZ8LQIbvl0myub +ZyiMH1LA/TURdJd0PtybHsp/r/vI3w8WrivMnQZv2z/VA5VFUrpqB0qaMWP/XJQm +RPebybxNseMHbRkLTnL1WnQgqqvurglmc1W96LecFh6MtaGswDs3RI/9wur63tY/ +4dijI/7yhfKoooU097RqRt0ObNW3BxGwNKUraMLKEZjtohv1cZBeRqzGZuui351E +YsG1jt23/3OP3Acfd1xpzoi+daadxl9JTr02kE7lMjfq32quhTdzuNZP84sQsaV+ +RXLNEoiSufjzy3nHTEpG6QaEWQc4gszCIBVRabxr7LtIOqJn2KmXxtOyFE52AJJj +ls3ifAECgYEA/9K+5oHdZBizWWUvNzQWXrdHUtVavCwjj+0v+yRFZSoAhKtVmLYl +8R4NeG6NCIOoJsqmGVpgtCyPndR4PQ6yr4Jt1FJorjsNw21eYrjOVG+y9Z0DkCwJ +uCRVUeqB42jLu7v9r1V3OBQdKLN6VxO4np05KEZyv1LOGGt0XC8NCykCgYEA/cC2 +NR7Y4Z5OjCc2GHMQNrVZ2HTDDir71RjcIIEmsIQ5/AMAELFLCSqzD73tJ87T5jPi +aWeOpIcnK78jMvIIsbV0BXaDsjtlvCdQui2AoX63FuK4q4E+vwe5Q/TqY2nDh2io +mGHfeXECyUx4gxIede2XEO9zYQ0lP8gxnjmLkFUCgYBO8LolqQcm/xRAzp9eOn14 +prekkN+Z10j1/avjpFKhn+9fAPu9zt8wYySm9/4fFXlK1xegFSpoDqQWgNzFgoaS +7/1yGifhM6nQlywb7IkGtx0S+2uBDoXFQ7jsOR/xi4HqoVzrwMS0EkjZKWDkA9rh +XwSnL+3yqduc33OdiotM2QKBgCgNCrVHsSOrQOqOJdOmFaEM7qljhIXv8t+nlNbs +i5bAyAYm0xPPZ/CCdNC/QXdPBdMHzWylk7YUPvKAsKWR3h1ubmmOUysGhQA1lGBO +XkcfIPbTwiIPvD+akHtRZM1cHCh7NGEY0ZTxaWcsUrkdWwFyBq39nVBsKrzudCZt +HsIhAoGBAMv3erZQawzIgX9nOUHB2UJl8pS+LZSNMlYvFoMHKI2hLq0g/VxUlnsh +Jzw9+fTLMVFdY+F3ydO6qQFd8wlfov7deyscdoSj8R5gjGKJsarBs+YVdFde2oLG +gkFsXmbmc2boyqGg51CbAX34VJOhGQKhWgKCWqDGmoYXafmyiZc+ +-----END RSA PRIVATE KEY----- +` + pemOpenSshRsa1024 = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAIEAzzknaHV741775aJOPacDpd2SiDpIDYmm7/w2sgY8lrinSakfLIVk +1qn0IBRLNOzMxoF/pvIgGQXS51xvE1vB3QK8L+8vJwH06DuOXPP1WgVoDTU03gGvBJ7MNF +5HcQYvBiIaU5XxG8l0OZO88B9RFhPP9r0XrYxAlSjuk9KKlEcAAAIYLQ46zy0OOs8AAAAH +c3NoLXJzYQAAAIEAzzknaHV741775aJOPacDpd2SiDpIDYmm7/w2sgY8lrinSakfLIVk1q +n0IBRLNOzMxoF/pvIgGQXS51xvE1vB3QK8L+8vJwH06DuOXPP1WgVoDTU03gGvBJ7MNF5H +cQYvBiIaU5XxG8l0OZO88B9RFhPP9r0XrYxAlSjuk9KKlEcAAAADAQABAAAAgQDJ9Jq6jF +08P/LhXug/38iHW0UW7S4Ru4jttHGd2MQt5DJtcJzIKA0ZxLL+nKibIPmFsOm2y5yKpolg +IE7EoBVzTeg0LedbRayc0Kc5tY7PEz0Shi9ABIMYbNo2L2pNmsq9ns0xA8ur3OugfKHsH8 +XjJ1rdHsyLjoMx2ADfLY0xkQAAAEAvyrgW4jswENdErbF0rOdP+Y73B/8rxBaY/QBE2qtG +oUp7bpOtUAH2Ip7RjXOX4xTAt4n2QeHBSfX7gfXRjmY6AAAAQQDmYlgSWtTYLV9VZSScLU +OG+GkhQxYqkKN/N9LSpTP4Pwh81KpMp40yvIlufmKLgGihWVxUDzRap3aoR7PqIvHPAAAA +QQDmQ47VwclxiVn5tVAht/Lk2ZVa7rSjeFlXAkAWZkUAiHboaH8IfW9W4gYV7o2BqJO11L +0vi+vCq+le45F416wJAAAAImNocmlzQHBvZXRhc3Rlci5jb3JwLm11dHVhbGluay5uZXQ= + +-----END OPENSSH PRIVATE KEY----- +` + pemOpenSshRsa2048 = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAxWfWNu0i8sbmwPqTUfKSeXOSt/fLMuqucn9KYU7rJ+83trznRhAn +AHQzKgcSU8PBgkax+PDEUexYUB9gZApNI6K/2twVDYh3Hgwx7EjXf05rji7bQk6TFyKEp4 +n348CWAdK8iFmNUutSpJLy7GciyMPLu3BK+EsXpsnuPpIm184hEFOiADZyHTGeUgvsKOAc +G7u5hBS3kty8LRZmL+pihbktFwGC4D5bapCcTaF2++zkUy4JKcVE5/2JfK1Ya6D0ATczjz +1b6+r7j2RUg1mXfK6AwMHEcamzhgeuM9RdrPtMdhZI09LCJzjmXc9pzlGu1HCZzh3rJ3hd +8PVmlAd3VQAAA+A9hesQPYXrEAAAAAdzc2gtcnNhAAABAQDFZ9Y27SLyxubA+pNR8pJ5c5 +K398sy6q5yf0phTusn7ze2vOdGECcAdDMqBxJTw8GCRrH48MRR7FhQH2BkCk0jor/a3BUN +iHceDDHsSNd/TmuOLttCTpMXIoSniffjwJYB0ryIWY1S61KkkvLsZyLIw8u7cEr4Sxemye +4+kibXziEQU6IANnIdMZ5SC+wo4Bwbu7mEFLeS3LwtFmYv6mKFuS0XAYLgPltqkJxNoXb7 +7ORTLgkpxUTn/Yl8rVhroPQBNzOPPVvr6vuPZFSDWZd8roDAwcRxqbOGB64z1F2s+0x2Fk +jT0sInOOZdz2nOUa7UcJnOHesneF3w9WaUB3dVAAAAAwEAAQAAAQEAvA8Z8iWjX6nA9yM/ +6ZevluhVY9E60XzlR8qgL2ehet/YMcxwfzywCyyn+WfXO9mHpfZ3YfLs9Ca2U04w4900c7 +h+EaAMpmHVKNjxTmpucadhq4hT9S0pz6ZgvcMgVuaHgaEjXroBencYuhQMPM5cQurUUfK+ +WSAgnhJNV2qgeoEGgfDZoL1HkItckEZwIzmx4lfMVAuaeqVq5tJNcdv5ukNHpnIYl6fgDp +WGUn/9F8sSHO7P7kGl67IZIsAz+1wW+6pFaVgxbZJ3baPiURtRp+nRSaKLYZSMph6MAiTu +YC8EEVqi3X4m/ZHy+BkphfzR24ouwpt1Vv9QOAPzXXsPwQAAAIEAvmA+yiBdzsJplCifTA +KljE+KpSuvLPRPTb7MGsBO0LIvxrkXOMCXZF4I2VP1zSUH+SDPPc6JeR1Q8liMqPC3Md6c +CIkHfVFBAZL709d0ZtTiir1BipG/l5vIpBnepNX/bWIszIOMzPF2at0WF1lFe6THWujuE8 +Xjp2AJSFZlUjAAAACBAOMxr6FN38VwRC1nrDcZyo4TjWVhAdk4p3AkdNJhFSiKS/x5/yo2 +K1majzcKbrR8+fEPTVWGszAg+AXQdsOq17q+DMenfrBckQ9ZHr3upSZAaGN+keNwge/Kaj +yOvYiKdYFXmAulQZCPQsDNp7e7Z1dTqxi5IlhUgDPzzO0vRGjNAAAAgQDeb0Ulv7fkYAav +tZ+D0LohGjlGFwTeLdwErcVnq9wGyupdeNhlTXZXxRvF+yEt4FCV9UEUnRX75CAnpk2hT2 +D5uYMyixAEfSeIo59Ln27MmAy0alR3UnT7JnLEZRh4dnvFbSSMJ1rHxf8Eg6YFJmpH65fX +exrJE+p69wgRVndoqQAAACJjaHJpc0Bwb2V0YXN0ZXIuY29ycC5tdXR1YWxpbmsubmV0AQ +IDBAUGBw== +-----END OPENSSH PRIVATE KEY----- +` + pemDsa = `-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQDH/T+IkpbdA9nUM7O4MMRoeS0bn7iXWs63Amo2fsIyJPxDvjjF +5HZBH5Rq045TFCCWHjymwiYof+wvwUMZIUH++ABTrKzes/r5qG5jXp42pFWf6nTI +zHwttdjvNiXr+AgreXOrJKhjv6Ga3hq8MNcXMa9xFsIB83EZNMBPxbj0nwIVAJQW +1eR4Uf8/8haQb4HkTsoH+R5/AoGBAK9FV5LIZxY1TeNsD5eXoqpTqCy1WROMggSG +VZ4yN0rrKCtLd8am61m/L8VCMUWiO3IJQdq3yWBTEBbsShL/toau9beUdTl6rdB8 +wcEcNgtZnhypQR58HlmgUFWC45rW37hW4AUJuMDgLxgqSVuoF1pDcHrHSi/fZwgp +7t0MKH2SAoGAJfUcLrXg5ZR8jbpZs4L/ubibUn+y35y+33aos07auMW1MesuNTcZ +Ch42nbH2wKnbjk8eDxHdHLHzzOLGgYVMpUuBeuc7G5Q94rM/Z0I8HGQ6mvIkuFyp +58Unu5yu33GsNUgGEHmriiMGezXNXGNH/72PmTXuyxEMSrad23c6NZoCFAtIqbal +4tGCfnnmWU514A7ZzEKj +-----END DSA PRIVATE KEY----- +` + pemEcdsa384 = `-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAjuEIlmFyhGjFtJoAwD420FuPAjIknN3YwDZL4cfMFpB4YAK+7QVLs +coAJ/ADuT7OgBwYFK4EEACKhZANiAASeXKyBr2prr4f4aOsM4dtVikYOUIL3yYnb +GFOy7yHmauCnkIB48paXpvRE5m53Q8zgu7vkz/z9tcMBcC0GzpY3Sef37fmgTUuZ +AJuJp36DMBdQel+j51TcQ79sizxCayg= +-----END EC PRIVATE KEY----- +` + pemEcdsa521 = `-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBVCiwcf/did2vCIu3aMe7OeTD35PULm0hqmfkAK9OKIosi/DjOFfA +8h99rVNPaf+Cx/JNmEzR4bZNnYDyilSRCr+gBwYFK4EEACOhgYkDgYYABABHBMLP +XbQoRF31ZGIeUj9jt9GqKES1dLBtGDEQSiiZFouL4tEIW7NfIZDpOIkA0khNcO8N +xH6eylg0XOgcr01GRwCjY5VOapOahtn63SpajPGeKk+46F2dULIwrov9tWQuYNa3 +P50N8j3rx6fAdgyDENOcCJlfNdNcySvkH4bgL1xcsw== +-----END EC PRIVATE KEY----- +` + pemOpenSshEd25519 = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAUftPhZQN17kAlThiiWJEgJvddm/pUhHvgrHUtpuYFOQAAAKjN+UhDzflI +QwAAAAtzc2gtZWQyNTUxOQAAACAUftPhZQN17kAlThiiWJEgJvddm/pUhHvgrHUtpuYFOQ +AAAEANXlEZdNU03RMmj77O2ojWh06Hbj8/qQ++H5wkt688NBR+0+FlA3XuQCVOGKJYkSAm +912b+lSEe+CsdS2m5gU5AAAAImNocmlzQHBvZXRhc3Rlci5jb3JwLm11dHVhbGluay5uZX +QBAgM= +-----END OPENSSH PRIVATE KEY----- +` +) + +func TestNewKeyPair_Default(t *testing.T) { + kp, err := NewKeyPair(CreateKeyPairConfig{}) + if err != nil { + t.Fatal(err.Error()) + } + + err = verifyEcdsaKeyPair(kp, expectedData{ + bits: 521, + }) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestNewKeyPair_ECDSA_Default(t *testing.T) { + kp, err := NewKeyPair(CreateKeyPairConfig{ + Type: Ecdsa, + }) + if err != nil { + t.Fatal(err.Error()) + } + + err = verifyEcdsaKeyPair(kp, expectedData{ + bits: 521, + }) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestNewKeyPair_ECDSA_Positive(t *testing.T) { + for _, bits := range []int{521, 384, 256} { + config := CreateKeyPairConfig{ + Type: Ecdsa, + Bits: bits, + Comment: uuid.TimeOrderedUUID(), + } + + kp, err := NewKeyPair(config) + if err != nil { + t.Fatal(err.Error()) + } + + err = verifyEcdsaKeyPair(kp, expectedData{ + bits: bits, + comment: config.Comment, + }) + if err != nil { + t.Fatal(err.Error()) + } + } +} + +func TestNewKeyPair_ECDSA_Negative(t *testing.T) { + for _, bits := range []int{224, 1, 2, 3} { + _, err := NewKeyPair(CreateKeyPairConfig{ + Type: Ecdsa, + Bits: bits, + }) + if err == nil { + t.Fatalf("expected key pair generation to fail for %d bits", bits) + } + } +} + +func TestNewKeyPair_RSA_Positive(t *testing.T) { + for _, bits := range []int{4096, 2048} { + config := CreateKeyPairConfig{ + Type: Rsa, + Bits: bits, + Comment: uuid.TimeOrderedUUID(), + } + + kp, err := NewKeyPair(config) + if err != nil { + t.Fatal(err.Error()) + } + + err = verifyRsaKeyPair(kp, expectedData{ + bits: config.Bits, + comment: config.Comment, + }) + if err != nil { + t.Fatal(err.Error()) + } + } +} + +func TestKeyPairFromPrivateKey(t *testing.T) { + m := map[string]fromPrivateExpectedData{ + pemRsa1024: { + t: Rsa, + d: expectedData{ + bits: 1024, + }, + }, + pemRsa2048: { + t: Rsa, + d: expectedData{ + bits: 2048, + }, + }, + pemOpenSshRsa1024: { + t: Rsa, + d: expectedData{ + bits: 1024, + }, + }, + pemOpenSshRsa2048: { + t: Rsa, + d: expectedData{ + bits: 2048, + }, + }, + pemDsa: { + t: Dsa, + d: expectedData{ + bits: 1024, + }, + }, + pemEcdsa384: { + t: Ecdsa, + d: expectedData{ + bits: 384, + }, + }, + pemEcdsa521: { + t: Ecdsa, + d: expectedData{ + bits: 521, + }, + }, + pemOpenSshEd25519: { + t: Ed25519, + d: expectedData{ + bits: 256, + }, + }, + } + + for rawPrivateKey, expected := range m { + kp, err := KeyPairFromPrivateKey(FromPrivateKeyConfig{ + RawPrivateKeyPemBlock: []byte(rawPrivateKey), + }) + if err != nil { + t.Fatal(err.Error()) + } + + switch expected.t { + case Dsa: + err = verifyDsaKeyPair(kp, expected) + case Ecdsa: + err = verifyEcdsaKeyPair(kp, expected.d) + case Ed25519: + err = verifyEd25519KeyPair(kp, expected) + case Rsa: + err = verifyRsaKeyPair(kp, expected.d) + default: + err = fmt.Errorf("unexected SSH key pair type %s", expected.t.String()) + } + if err != nil { + t.Fatal(err.Error()) + } + } +} + +type fromPrivateExpectedData struct { + t KeyPairType + d expectedData +} + +type expectedData struct { + bits int + comment string +} + +func verifyEcdsaKeyPair(kp KeyPair, e expectedData) error { + privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock) + if err != nil { + return err + } + + pk, ok := privateKey.(*ecdsa.PrivateKey) + if !ok { + return fmt.Errorf("private key should be *ecdsa.PrivateKey") + } + + if pk.Curve.Params().BitSize != e.bits { + return fmt.Errorf("bit size should be %d - got %d", e.bits, pk.Curve.Params().BitSize) + } + + publicKey, err := gossh.NewPublicKey(&pk.PublicKey) + if err != nil { + return err + } + + expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n")) + if len(e.comment) > 0 { + expectedBytes = append(expectedBytes, ' ') + expectedBytes = append(expectedBytes, e.comment...) + } + + if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) { + return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'", + string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine)) + } + + return nil +} + +func verifyRsaKeyPair(kp KeyPair, e expectedData) error { + privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock) + if err != nil { + return err + } + + pk, ok := privateKey.(*rsa.PrivateKey) + if !ok { + return fmt.Errorf("private key should be *rsa.PrivateKey") + } + + if pk.N.BitLen() != e.bits { + return fmt.Errorf("bit size should be %d - got %d", e.bits, pk.N.BitLen()) + } + + publicKey, err := gossh.NewPublicKey(&pk.PublicKey) + if err != nil { + return err + } + + expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n")) + if len(e.comment) > 0 { + expectedBytes = append(expectedBytes, ' ') + expectedBytes = append(expectedBytes, e.comment...) + } + + if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) { + return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'", + string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine)) + } + + return nil +} + +func verifyDsaKeyPair(kp KeyPair, e fromPrivateExpectedData) error { + privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock) + if err != nil { + return err + } + + pk, ok := privateKey.(*dsa.PrivateKey) + if !ok { + return fmt.Errorf("private key should be *rsa.PrivateKey") + } + + publicKey, err := gossh.NewPublicKey(&pk.PublicKey) + if err != nil { + return err + } + + expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n")) + if len(e.d.comment) > 0 { + expectedBytes = append(expectedBytes, ' ') + expectedBytes = append(expectedBytes, e.d.comment...) + } + + if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) { + return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'", + string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine)) + } + + return nil +} + +func verifyEd25519KeyPair(kp KeyPair, e fromPrivateExpectedData) error { + privateKey, err := gossh.ParseRawPrivateKey(kp.PrivateKeyPemBlock) + if err != nil { + return err + } + + pk, ok := privateKey.(*ed25519.PrivateKey) + if !ok { + return fmt.Errorf("private key should be *rsa.PrivateKey") + } + + publicKey, err := gossh.NewPublicKey(pk.Public()) + if err != nil { + return err + } + + expectedBytes := bytes.TrimSuffix(gossh.MarshalAuthorizedKey(publicKey), []byte("\n")) + if len(e.d.comment) > 0 { + expectedBytes = append(expectedBytes, ' ') + expectedBytes = append(expectedBytes, e.d.comment...) + } + + if !bytes.Equal(expectedBytes, kp.PublicKeyAuthorizedKeysLine) { + return fmt.Errorf("authorized keys line should be:\n'%s'\nGot:\n'%s'", + string(expectedBytes), string(kp.PublicKeyAuthorizedKeysLine)) + } + + return nil +} diff --git a/website/source/docs/builders/virtualbox-iso.html.md.erb b/website/source/docs/builders/virtualbox-iso.html.md.erb index e7ec0b84d..7d1a9dcbf 100644 --- a/website/source/docs/builders/virtualbox-iso.html.md.erb +++ b/website/source/docs/builders/virtualbox-iso.html.md.erb @@ -381,6 +381,8 @@ contention. If you notice missing keys, you can tune this delay by specifying <%= partial "partials/builders/boot-command" %> +<%= partial "partials/builders/virtualbox-ssh-key-pair" %> + Example boot command. This is actually a working boot command used to start an Ubuntu 12.04 installer: diff --git a/website/source/docs/builders/virtualbox-ovf.html.md.erb b/website/source/docs/builders/virtualbox-ovf.html.md.erb index 379e5cb8a..e42198b4d 100644 --- a/website/source/docs/builders/virtualbox-ovf.html.md.erb +++ b/website/source/docs/builders/virtualbox-ovf.html.md.erb @@ -321,6 +321,8 @@ contention. If you notice missing keys, you can tune this delay by specifying <%= partial "partials/builders/boot-command" %> +<%= partial "partials/builders/virtualbox-ssh-key-pair" %> + Example boot command. This is actually a working boot command used to start an Ubuntu 12.04 installer: diff --git a/website/source/partials/builders/_virtualbox-ssh-key-pair.html.md b/website/source/partials/builders/_virtualbox-ssh-key-pair.html.md new file mode 100644 index 000000000..62372174c --- /dev/null +++ b/website/source/partials/builders/_virtualbox-ssh-key-pair.html.md @@ -0,0 +1,61 @@ +### SSH key pair automation + +The VirtualBox builders can inject the current SSH key pair's public key into +the template using the following variable: + +- `SSHPublicKey` (*VirtualBox builders only*) - This is the SSH public key + as a line in OpenSSH authorized_keys format. + +When a private key is provided using `ssh_private_key_file`, the key's +corresponding public key can be accessed using the above variables. + +If `ssh_password` and `ssh_private_key_file` are not specified, Packer will +automatically generate en ephemeral key pair. The key pair's public key can +be accessed using the template variables. + +For example, the public key can be provided in the boot command as a URL +encoded string by appending `| urlquery` to the variable: +```json +{ + "type": "virtualbox-iso", + "boot_command": [ + " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg PACKER_USER={{ user `username` }} PACKER_AUTHORIZED_KEY={{ .SSHPublicKey | urlquery }}" + ] +} +``` + +A kickstart could then leverage those fields from the kernel command line by +decoding the URL-encoded public key: +``` +%post + +# Newly created users need the file/folder framework for SSH key authentication. +umask 0077 +mkdir /etc/skel/.ssh +touch /etc/skel/.ssh/authorized_keys + +# Loop over the command line. Set interesting variables. +for x in $(cat /proc/cmdline) +do + case $x in + PACKER_USER=*) + PACKER_USER="${x#*=}" + ;; + PACKER_AUTHORIZED_KEY=*) + # URL decode $encoded into $PACKER_AUTHORIZED_KEY + encoded=$(echo "${x#*=}" | tr '+' ' ') + printf -v PACKER_AUTHORIZED_KEY '%b' "${encoded//%/\\x}" + ;; + esac +done + +# Create/configure packer user, if any. +if [ -n "$PACKER_USER" ] +then + useradd $PACKER_USER + echo "%$PACKER_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/$PACKER_USER + [ -n "$PACKER_AUTHORIZED_KEY" ] && echo $PACKER_AUTHORIZED_KEY >> $(eval echo ~"$PACKER_USER")/.ssh/authorized_keys +fi + +%end +```