From 53b3867c507ffd01f8a0c5d85230ea3ac8a5e3f4 Mon Sep 17 00:00:00 2001 From: Ali Hamidi Date: Wed, 23 Mar 2016 10:35:47 -0700 Subject: [PATCH 1/5] allow packer to create an encrypted copy of the AMI --- builder/amazon/common/ami_config.go | 1 + builder/amazon/ebs/builder.go | 1 + builder/amazon/ebs/step_encrypted_ami.go | 114 ++++++++++++++++++ .../source/docs/builders/amazon-ebs.html.md | 4 + 4 files changed, 120 insertions(+) create mode 100644 builder/amazon/ebs/step_encrypted_ami.go diff --git a/builder/amazon/common/ami_config.go b/builder/amazon/common/ami_config.go index 078dbefaa..0d5c584c2 100644 --- a/builder/amazon/common/ami_config.go +++ b/builder/amazon/common/ami_config.go @@ -19,6 +19,7 @@ type AMIConfig struct { AMITags map[string]string `mapstructure:"tags"` AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"` AMIForceDeregister bool `mapstructure:"force_deregister"` + AMIEncryptBootVolume bool `mapstructure:"encrypt_boot"` } func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error { diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 638efa5a3..ff54cee12 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -162,6 +162,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe AMIName: b.config.AMIName, }, &stepCreateAMI{}, + &stepCreateEncryptedAMICopy{}, &awscommon.StepAMIRegionCopy{ AccessConfig: &b.config.AccessConfig, Regions: b.config.AMIRegions, diff --git a/builder/amazon/ebs/step_encrypted_ami.go b/builder/amazon/ebs/step_encrypted_ami.go new file mode 100644 index 000000000..a8f8b25da --- /dev/null +++ b/builder/amazon/ebs/step_encrypted_ami.go @@ -0,0 +1,114 @@ +package ebs + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/packer" +) + +type stepCreateEncryptedAMICopy struct { + image *ec2.Image +} + +func (s *stepCreateEncryptedAMICopy) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packer.Ui) + + // Encrypt boot not set, so skip step + if !config.AMIConfig.AMIEncryptBootVolume { + return multistep.ActionContinue + } + + ui.Say("Creating Encrypted AMI Copy") + + amis := state.Get("amis").(map[string]string) + var region, id string + if amis != nil { + for region, id = range amis { + break // Only get the first + } + } + + ui.Say(fmt.Sprintf("Copying AMI: %s(%s)", region, id)) + + copyOpts := &ec2.CopyImageInput{ + Name: &config.AMIName, // Try to overwrite existing AMI + SourceImageId: aws.String(id), + SourceRegion: aws.String(region), + Encrypted: aws.Bool(true), + } + + copyResp, err := ec2conn.CopyImage(copyOpts) + if err != nil { + err := fmt.Errorf("Error copying AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Wait for the copy to become ready + stateChange := awscommon.StateChangeConf{ + Pending: []string{"pending"}, + Target: "available", + Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *copyResp.ImageId), + StepState: state, + } + + ui.Say("Waiting for AMI copy to become ready...") + if _, err := awscommon.WaitForState(&stateChange); err != nil { + err := fmt.Errorf("Error waiting for AMI Copy: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Remove unencrypted AMI + ui.Say("Deregistering unecrypted AMI") + deregisterOpts := &ec2.DeregisterImageInput{ImageId: aws.String(id)} + if _, err := ec2conn.DeregisterImage(deregisterOpts); err != nil { + ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", err)) + return multistep.ActionHalt + } + + // Replace original AMI ID with Encrypted ID in state + amis[region] = *copyResp.ImageId + state.Put("amis", amis) + + imagesResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{copyResp.ImageId}}) + if err != nil { + err := fmt.Errorf("Error searching for AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + s.image = imagesResp.Images[0] + + return multistep.ActionContinue +} + +func (s *stepCreateEncryptedAMICopy) Cleanup(state multistep.StateBag) { + if s.image == nil { + return + } + + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + if !cancelled && !halted { + return + } + + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deregistering the AMI because cancelation or error...") + deregisterOpts := &ec2.DeregisterImageInput{ImageId: s.image.ImageId} + if _, err := ec2conn.DeregisterImage(deregisterOpts); err != nil { + ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", err)) + return + } +} diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index a11f8cbba..f71cb4b2b 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -140,6 +140,10 @@ builder. - `force_deregister` (boolean) - Force Packer to first deregister an existing AMI if one with the same name already exists. Default `false`. +- `encrypt_boot` (boolean) - Instruct packer to automatically create a copy of the + AMI with an encrypted boot volume (discarding the initial unencrypted AMI in the + process). + - `iam_instance_profile` (string) - The name of an [IAM instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html) to launch the EC2 instance with. From 902ae375b3b6420522b4cceb85ee4c71b084b7de Mon Sep 17 00:00:00 2001 From: Ali Hamidi Date: Wed, 23 Mar 2016 16:21:12 -0700 Subject: [PATCH 2/5] delete unencrypted snapshot --- builder/amazon/ebs/step_encrypted_ami.go | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/builder/amazon/ebs/step_encrypted_ami.go b/builder/amazon/ebs/step_encrypted_ami.go index a8f8b25da..ef7e04ade 100644 --- a/builder/amazon/ebs/step_encrypted_ami.go +++ b/builder/amazon/ebs/step_encrypted_ami.go @@ -67,6 +67,16 @@ func (s *stepCreateEncryptedAMICopy) Run(state multistep.StateBag) multistep.Ste return multistep.ActionHalt } + // Get the unencrypted AMI image + unencImagesResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{aws.String(id)}}) + if err != nil { + err := fmt.Errorf("Error searching for AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + unencImage := unencImagesResp.Images[0] + // Remove unencrypted AMI ui.Say("Deregistering unecrypted AMI") deregisterOpts := &ec2.DeregisterImageInput{ImageId: aws.String(id)} @@ -75,6 +85,24 @@ func (s *stepCreateEncryptedAMICopy) Run(state multistep.StateBag) multistep.Ste return multistep.ActionHalt } + // Remove associated unencrypted snapshot(s) + ui.Say("Deleting unencrypted snapshots") + + for _, blockDevice := range unencImage.BlockDeviceMappings { + if blockDevice.Ebs != nil { + if blockDevice.Ebs.SnapshotId != nil { + ui.Message(fmt.Sprintf("Snapshot ID: %s", *blockDevice.Ebs.SnapshotId)) + deleteSnapOpts := &ec2.DeleteSnapshotInput{ + SnapshotId: aws.String(*blockDevice.Ebs.SnapshotId), + } + if _, err := ec2conn.DeleteSnapshot(deleteSnapOpts); err != nil { + ui.Error(fmt.Sprintf("Error deleting snapshot, may still be around: %s", err)) + return multistep.ActionHalt + } + } + } + } + // Replace original AMI ID with Encrypted ID in state amis[region] = *copyResp.ImageId state.Put("amis", amis) From 1cecda639ab09c3ec07e7ec92217f5446e3484c7 Mon Sep 17 00:00:00 2001 From: Ali Hamidi Date: Fri, 25 Mar 2016 11:56:17 -0700 Subject: [PATCH 3/5] add encrypted boot volume acceptance test --- builder/amazon/ebs/builder_acc_test.go | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/builder/amazon/ebs/builder_acc_test.go b/builder/amazon/ebs/builder_acc_test.go index 47da06c10..b77e5853b 100644 --- a/builder/amazon/ebs/builder_acc_test.go +++ b/builder/amazon/ebs/builder_acc_test.go @@ -55,6 +55,15 @@ func TestBuilderAcc_amiSharing(t *testing.T) { }) } +func TestBuilderAcc_encryptedBoot(t *testing.T) { + builderT.Test(t, builderT.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Builder: &Builder{}, + Template: testBuilderAccEncrypted, + Check: checkBootEncrypted(), + }) +} + func checkAMISharing(count int, uid, group string) builderT.TestCheckFunc { return func(artifacts []packer.Artifact) error { if len(artifacts) > 1 { @@ -144,6 +153,42 @@ func checkRegionCopy(regions []string) builderT.TestCheckFunc { } } +func checkBootEncrypted() builderT.TestCheckFunc { + return func(artifacts []packer.Artifact) error { + + // Get the actual *Artifact pointer so we can access the AMIs directly + artifactRaw := artifacts[0] + artifact, ok := artifactRaw.(*common.Artifact) + if !ok { + return fmt.Errorf("unknown artifact: %#v", artifactRaw) + } + + // describe the image, get block devices with a snapshot + ec2conn, _ := testEC2Conn() + imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ + ImageIds: []*string{aws.String(artifact.Amis["us-east-1"])}, + }) + + if err != nil { + return fmt.Errorf("Error retrieving Image Attributes for AMI (%s) in AMI Encrypted Boot Test: %s", artifact, err) + } + + image := imageResp.Images[0] // Only requested a single AMI ID + + rootDeviceName := image.RootDeviceName + + for _, bd := range image.BlockDeviceMappings { + if *bd.DeviceName == *rootDeviceName { + if *bd.Ebs.Encrypted != true { + return fmt.Errorf("volume not encrypted: %s", *bd.Ebs.SnapshotId) + } + } + } + + return nil + } +} + func testAccPreCheck(t *testing.T) { if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" { t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests") @@ -222,6 +267,20 @@ const testBuilderAccSharing = ` } ` +const testBuilderAccEncrypted = ` +{ + "builders": [{ + "type": "test", + "region": "us-east-1", + "instance_type": "m3.medium", + "source_ami":"ami-c15bebaa", + "ssh_username": "ubuntu", + "ami_name": "packer-enc-test {{timestamp}}", + "encrypt_boot": true + }] +} +` + func buildForceDeregisterConfig(name, flag string) string { return fmt.Sprintf(testBuilderAccForceDeregister, name, flag) } From 6b7ed3aaaba2f23260878b6604a347655752d617 Mon Sep 17 00:00:00 2001 From: Ali Hamidi Date: Mon, 28 Mar 2016 09:32:39 -0700 Subject: [PATCH 4/5] include default value in docs --- website/source/docs/builders/amazon-ebs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index f71cb4b2b..5d8f21887 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -142,7 +142,7 @@ builder. - `encrypt_boot` (boolean) - Instruct packer to automatically create a copy of the AMI with an encrypted boot volume (discarding the initial unencrypted AMI in the - process). + process). Default `false`. - `iam_instance_profile` (string) - The name of an [IAM instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html) From c6a527dc46d48887d858127996b2c79e77e44687 Mon Sep 17 00:00:00 2001 From: Ali Hamidi Date: Thu, 12 May 2016 09:21:44 -0700 Subject: [PATCH 5/5] throw error if encrypted ami is shared --- builder/amazon/common/ami_config.go | 4 ++++ builder/amazon/common/ami_config_test.go | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/builder/amazon/common/ami_config.go b/builder/amazon/common/ami_config.go index 0d5c584c2..3bbe8039a 100644 --- a/builder/amazon/common/ami_config.go +++ b/builder/amazon/common/ami_config.go @@ -55,6 +55,10 @@ func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error { c.AMIRegions = regions } + if len(c.AMIUsers) > 0 && c.AMIEncryptBootVolume { + errs = append(errs, fmt.Errorf("Cannot share AMI with encrypted boot volume")) + } + if len(errs) > 0 { return errs } diff --git a/builder/amazon/common/ami_config_test.go b/builder/amazon/common/ami_config_test.go index 77158c41c..9d52cfc3b 100644 --- a/builder/amazon/common/ami_config_test.go +++ b/builder/amazon/common/ami_config_test.go @@ -58,3 +58,12 @@ func TestAMIConfigPrepare_regions(t *testing.T) { c.AMISkipRegionValidation = false } + +func TestAMIConfigPrepare_EncryptBoot(t *testing.T) { + c := testAMIConfig() + c.AMIUsers = []string{"testAccountID"} + c.AMIEncryptBootVolume = true + if err := c.Prepare(nil); err == nil { + t.Fatal("should have error") + } +}