Merge pull request #7287 from stephen-fox/ephemeral-ssh-key-pair-issue-7225

virtualbox: create ephemeral SSH key pair for build process
This commit is contained in:
Megan Marsh 2019-03-04 14:25:04 -08:00 committed by GitHub
commit 3dc1dafe58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 928 additions and 8 deletions

View File

@ -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))
}
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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)
}

258
helper/ssh/key_pair.go Normal file
View File

@ -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
}

449
helper/ssh/key_pair_test.go Normal file
View File

@ -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
}

View File

@ -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:

View File

@ -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:

View File

@ -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": [
"<up><wait><tab> text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg PACKER_USER={{ user `username` }} PACKER_AUTHORIZED_KEY={{ .SSHPublicKey | urlquery }}<enter>"
]
}
```
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
```