Dave Konopka

Encrypted Amazon EC2 boot volumes with Packer and Ansible

At the end of 2015 Amazon added support for encrypted EBS boot volumes. EBS storage volumes had offered optional encryption for some time before that. Now it’s possible to encrypt an AMI and bring up EC2 instances with fully encrypted starting volumes.

I set out recently to use encrypted EBS boot volumes for a HIPAA compliant project at ReactiveOps. It’s very easy to convert an existing AMI with an unencrypted boot volume to use encryption. I hit a few snags though building encryption into my automated AMI generation workflows using Packer and Ansible tooling.

Encrypt an existing AMI boot volume

Once you have an AMI with an encrypted boot volume any EC2 instance you launch using that AMI will have its boot volume encrypted. There’s nothing extra to do at instance launch time.

If you already have an AMI with an unencrypted boot volume it’s easy to encrypt it. This feature piggybacks on the ability to encrypt EBS volume snapshots while copying them. You can enable encryption for an AMI by copying it using the CLI, API, or the Amazon web panel and enabling encryption.

Amazon’s announcement post contains detailed instructions for copying an AMI using either the CLI or the web panel. After you make the copy you will have a new and separate AMI. Any EC2 instance you start from this new AMI will have an encrypted boot volume. There’s nothing special to do at instance launch.

Marketplace AMI’s

The first hitch I ran into involved AMI’s from the AWS Marketplace. For most projects I start with a base operating system AMI maintained by the OS’s official backing organization. Canonical publishes Ubuntu images. CentOS.org publishes CentOS images.

Unfortunately Amazon doesn’t allow AMI’s with a product code to be copied. All marketplace images have a product code. This makes it impossible to encrypt a marketplace sourced AMI by copying it into your account. The alternative is to stand up an EC2 instance from the marketplace AMI and create an AMI from the instance. The resulting AMI copy can then itself be copied and encrypted. That’s a lot of copying.

Side note: Ubuntu publishes a “Cloud Image” library of their official AMI’s. These AMI’s are separate from the AWS Marketplace. They can be copied with encryption freely unlike the marketplace versions.

Packer amazon-ebs encryption is not possible

I tend to use Packer’s amazon-ebs to build AMI’s. I had hopes for a Packer native option to produce an encrypted boot volume from a marketplace AMI. Though you can set an encryption field on volumes using the ami_block_device_mappings this doesn’t produce an encrypted EBS snapshot for the boot volume. It does not appear there is a way to do this natively with Packer. I had to resort to following up Packer AMI builds with a separate process to copy the resulting unencrypted AMI to an encrypted format.

Creating encrypted AMI’s with Ansible

The following Ansible playbook copies an unencrypted AMI and encrypts it. It tags the resulting AMI with Encrypted='true'. Ansible 2.0 or greater is required.

Ansible has an ec2_ami_copy module. Due to limitations in Boto though this module does not yet support enabling encryption on copy. There are pending PR’s for bringing this feature to the module. Until those are released, this code shells out to the AWS CLI to create the AMI copy. You’ll need to configure the AWS CLI with appropriate credentials on your machine to use this playbook.


---

- name: Create an encrypted copy of a given AMI
  hosts: localhost
  connection: local
  gather_facts: False
  vars:
    aws_region: "us-east-1"
    unencrypted_ami_id: "ami-XXXXXXXX"

  tasks:
    - name: Find the latest AMI by tags
      ec2_ami_find:
        owner: self
        ami_id: "{{ unencrypted_ami_id }}"
        no_result_action: fail
        region: "{{ aws_region }}"
        sort: name
        sort_order: descending
        sort_end: 1
      register: latest_ami

    - set_fact: unencrypted_ami_name="{{ latest_ami.results[0].name }}"

    - name: Check if encrypted copy of AMI already exists
      ec2_ami_find:
        owner: self
        name: "{{ unencrypted_ami_name }}"
        ami_tags:
          Encrypted: "true"
        no_result_action: success
        region: "{{ aws_region }}"
      register: latest_ami_encrypted

    # Proceed with encrypting only if no encrypted copy already exists
    - block:
        - name: Encrypt AMI by copying it
          shell: "aws ec2 copy-image --source-region '{{ aws_region }}' --region '{{ aws_region }}' --encrypted --source-image-id '{{ unencrypted_ami_id }}' --name '{{ unencrypted_ami_name }}'"
          register: ami_copy

        - set_fact: ami_copy_json="{{ ami_copy.stdout|from_json }}"
        - set_fact: encrypted_ami_id="{{ ami_copy_json['ImageId'] }}"

        # Retry until ami is available for tagging
        - name: Confirm encrypted AMI id.
          ec2_ami_find:
            owner: self
            ami_id: "{{ encrypted_ami_id }}"
            no_result_action: success
            region: "{{ aws_region }}"
            state: pending
          register: confirm_ami_encrypted
          until: confirm_ami_encrypted.results|length > 0
          retries: 4
          delay: 15

        - name: Tag encrypted AMI
          ec2_tag:
            region: "{{ aws_region }}"
            resource: "{{ item.ami_id }}"
            state: present
            tags:
              Name: "{{ unencrypted_ami_name }}"
              Encrypted: "true"
          with_items: confirm_ami_encrypted.results

      when: latest_ami.results|length == 1 and latest_ami_encrypted.results|length == 0