My Journey to the cloud…

In pursuit of excellence….


Encrypt EBS Volume for an already launched EC2 Instance

  Why Encrypt EBS Volumes?

Encryption of EBS volumes is considered a security best practice for several reasons. Encryption protects sensitive data at rest in your EBS volumes from unauthorized access if the volumes become detached from an instance. Encryption uses AES-256 cryptographic algorithm to encrypt your volumes, which meets rigorous security standards. You maintain ownership of encryption keys, rather than AWS managing the keys. Overall, encrypting EBS volumes gives you an extra layer of data protection and helps ensure compliance with regulations.

Encrypting Existing Volumes

Encrypting EBS volumes already attached to running EC2 instances is straightforward. Here are the steps:

– Create a snapshot of the volume to encrypt. This snapshot serves as the basis for the encrypted volume.

– Create a copy of the snapshot and select the option to encrypt it. The encryption process may take a while to complete.

– Create a new EBS volume from the encrypted snapshot.

– Detach the original unencrypted volume from the instance.

– Attach the new encrypted volume to the instance.

– Verify the volume is now encrypted by checking from within the instance.

Maintaining Encryption

Once a volume is encrypted, any snapshots made of it will also be encrypted automatically. Any volumes created from those snapshots will inherit encryption as well. This helps maintain encryption through the lifetime of your EBS volumes.

When launching new EC2 instances, you can also opt to have their EBS volumes encrypted by default via settings in the AMI or block device mapping.  

Take Action

Encrypting EBS volumes is a simple way to add an extra layer of security. Follow the steps outlined here to start encrypting your existing volumes today.

Encryption Script Overview

The provided Python script will:

– Check the EC2 instance is running and if running, will stop the instance.

– Create a snapshot of each attached unencrypted EBS volume

– Create an encrypted copy of each snapshot

– Create encrypted volumes from the encrypted snapshots

– Detach original unencrypted volumes

– Attach new encrypted volumes

– Clean up unneeded snapshots

Running the Script

To run the script:

1. Clone or download the Python script

2. Install dependencies like Boto3

3. Update the script configuration variables

4. Execute `python -i <Intsance_ID> -key <KMS_KeyID> -r <AWS_Regon>`

The script will output logs in HTML format showing the status of each step.

#!/usr/bin/python

"""
Overview:
    Iterate through each attached volume and encrypt it for EC2.
Params:
    ID for EC2 instance
    Customer Master Key (CMK) (optional)
    Profile to use
Conditions:
    Return if volume already encrypted
    Use named profiles from credentials file
"""

import sys
import boto3
import botocore
import argparse

""" Below piece of code is added to redirect the output to stdout as well as logfile """
class Logger(object):
    def __init__(self):
        self.terminal = sys.stdout
        self.log = open("/tmp/encryptvolume.log", "a")

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)  

    def flush(self):
        #this flush method is needed for python 3 compatibility.
        #this handles the flush command by doing nothing.
        #you might want to specify some extra behavior here.
        pass    

sys.stdout = Logger()
""" logger code Ends """

def main(argv):
    parser = argparse.ArgumentParser(description='Encrypts EC2 root volume.')
    parser.add_argument('-i', '--instance',
                        help='Instance to encrypt volume on.', required=True)
    parser.add_argument('-key', '--customer_master_key',
                        help='Customer master key', required=True)
    parser.add_argument('-p', '--profile',
                        help='Profile to use', required=False)
    parser.add_argument('-r', '--region',
                        help='Region of source volume', required=True)
    args = parser.parse_args()

    """ Set up AWS Session + Client + Resources + Waiters """
    if args.profile:
        # Create custom session
        print('Using profile {}'.format(args.profile))
        session = boto3.session.Session(profile_name=args.profile)
    else:
        # Use default session
        session = boto3.session.Session()

    # Get CMK
    customer_master_key = args.customer_master_key

    client = session.client('ec2')
    ec2 = session.resource('ec2')

    waiter_instance_exists = client.get_waiter('instance_exists')
    waiter_instance_stopped = client.get_waiter('instance_stopped')
    waiter_instance_running = client.get_waiter('instance_running')
    waiter_snapshot_complete = client.get_waiter('snapshot_completed')
    waiter_volume_available = client.get_waiter('volume_available')

    """ Check instance exists """
    instance_id = args.instance
    #print('---Checking instance ({})'.format(instance_id))
    print('<font size=1 face=verdana color=blue>---Checking instance <b><font size=1 color=red>({})</font></b></font><br>'.format(instance_id))
    instance = ec2.Instance(instance_id)

    try:
        waiter_instance_exists.wait(
            InstanceIds=[
                instance_id,
            ]
        )
    except botocore.exceptions.WaiterError as e:
        sys.exit('ERROR: {}'.format(e))
    
    all_mappings = []    
    
    block_device_mappings = instance.block_device_mappings

    for device_mapping in block_device_mappings:
        original_mappings = {
            'DeleteOnTermination': device_mapping['Ebs']['DeleteOnTermination'],
            'VolumeId': device_mapping['Ebs']['VolumeId'],
            'DeviceName': device_mapping['DeviceName'],
        }
        all_mappings.append(original_mappings)
  
    volume_data = []
    
    #print('---Preparing instance')    
    print('<font size=1 face=verdana color=blue>---Preparing instance</font><br>')
    """ Get volume and exit if already encrypted """
    #volumes = [v for v in instance.volumes.all()]
    # Below code is commented to take care of Citrix Server Encryption Issue - 7th Sep 2020
    # Once the issue is resolved, will have to revert the code
    # this is to encrypt the already encrypted volume using Citirx Account local KMS key and not
    # KMS Account Key
    volumes =  [v for v in instance.volumes.filter(Filters=[{'Name': 'encrypted', 'Values': ['false']}])]
    for volume in volumes:
        volume_encrypted = volume.encrypted
        #print(volume.volume_id)
        
        current_volume_data = {}
        for mapping in all_mappings:
            if mapping['VolumeId'] == volume.volume_id:
                current_volume_data = {
                    'volume': volume,
                    'DeleteOnTermination': mapping['DeleteOnTermination'],
                    'DeviceName': mapping['DeviceName'],
                }        
                 
        if volume_encrypted:
            sys.exit(
                '**Volume ({}) is already encrypted'
                .format(volume.id))

        """ Step 1: Prepare instance """
    
        # Exit if instance is pending, shutting-down, or terminated
        instance_exit_states = [0, 32, 48]
        if instance.state['Code'] in instance_exit_states:
            sys.exit(
                'ERROR: Instance is {} please make sure this instance is active.'
                .format(instance.state['Name'])
            )
    
        # Validate successful shutdown if it is running or stopping
        if instance.state['Code'] is 16:
            instance.stop()
    
        # Set the max_attempts for this waiter (default 40)
        waiter_instance_stopped.config.max_attempts = 80
    
        try:
            waiter_instance_stopped.wait(
                InstanceIds=[
                    instance_id,
                ]
            )
        except botocore.exceptions.WaiterError as e:
            sys.exit('ERROR: {}'.format(e))
    
        """ Step 2: Take snapshot of volume """
        #print('---Create snapshot of volume ({})'.format(volume.id))
        print('<font size=1 face=verdana color=blue>---Create snapshot of volume <b><font size=1 color=red>({})</font></b></font><br>'.format(volume.id))
        snapshot = ec2.create_snapshot(
            VolumeId=volume.id,
            Description='Snapshot of volume ({})'.format(volume.id),
        )
        
        waiter_snapshot_complete.config.max_attempts = 240
    
        try:
            waiter_snapshot_complete.wait(
                SnapshotIds=[
                    snapshot.id,
                ]
            )
        except botocore.exceptions.WaiterError as e:
            snapshot.delete()
            sys.exit('ERROR: {}'.format(e))
    
        """ Step 3: Create encrypted volume """
        #print('---Create encrypted copy of snapshot')
        print('<font size=1 face=verdana color=blue>---Create encrypted copy of snapshot</font><br>')
        if customer_master_key:
            # Use custom key
            snapshot_encrypted_dict = snapshot.copy(
                SourceRegion=args.region,
                Description='Encrypted copy of snapshot #{}'
                            .format(snapshot.id),
                KmsKeyId=customer_master_key,
                Encrypted=True,
            )
        else:
            # Use default key
            snapshot_encrypted_dict = snapshot.copy(
                SourceRegion=args.region,
                Description='Encrypted copy of snapshot ({})'
                            .format(snapshot.id),
                Encrypted=True,
            )
    
        snapshot_encrypted = ec2.Snapshot(snapshot_encrypted_dict['SnapshotId'])
    
        try:
            waiter_snapshot_complete.wait(
                SnapshotIds=[
                    snapshot_encrypted.id,
                ],
            )
        except botocore.exceptions.WaiterError as e:
            snapshot.delete()
            snapshot_encrypted.delete()
            sys.exit('ERROR: {}'.format(e))
    
        #print('---Create encrypted volume from snapshot')
        print('<font size=1 face=verdana color=blue>---Create encrypted volume from snapshot</font><br>')

        if volume.volume_type == 'io1':
            volume_encrypted = ec2.create_volume(
                SnapshotId=snapshot_encrypted.id,
                VolumeType=volume.volume_type,
                Iops=volume.iops,
                AvailabilityZone=instance.placement['AvailabilityZone']
            )
        else:
            volume_encrypted = ec2.create_volume(
                SnapshotId=snapshot_encrypted.id,
                VolumeType=volume.volume_type,
                AvailabilityZone=instance.placement['AvailabilityZone']
            )
     
        # Add original tags to new volume 
        if volume.tags:
            volume_encrypted.create_tags(Tags=volume.tags)
    
        """ Step 4: Detach current volume """
        #print('---Detach volume {}'.format(volume.id))
        print('<font size=1 face=verdana color=blue>---Detach volume <b><font size=1 color=red> {}</font></b></font><br>'.format(volume.id))
        instance.detach_volume(
            VolumeId=volume.id,
            Device=current_volume_data['DeviceName']
        )
    
        """ Step 5: Attach new encrypted volume """
        #print('---Attach volume {}'.format(volume_encrypted.id))
        print('<font size=1 face=verdana color=blue>---Attach volume <b><font size=1 color=red> {}</font></b></font><br>'.format(volume_encrypted.id))
        try:
            waiter_volume_available.wait(
                VolumeIds=[
                    volume_encrypted.id,
                ],
            )
        except botocore.exceptions.WaiterError as e:
            snapshot.delete()
            snapshot_encrypted.delete()
            volume_encrypted.delete()
            sys.exit('ERROR: {}'.format(e))
    
        instance.attach_volume(
            VolumeId=volume_encrypted.id,
            Device=current_volume_data['DeviceName']
        )
        
        current_volume_data['snapshot'] = snapshot
        current_volume_data['snapshot_encrypted'] = snapshot_encrypted
        volume_data.append(current_volume_data)                  
    
    for bdm in volume_data:
        # Modify instance attributes
        instance.modify_attribute(
            BlockDeviceMappings=[
                {
                    'DeviceName': bdm['DeviceName'],
                    'Ebs': {
                        'DeleteOnTermination':
                        bdm['DeleteOnTermination'],
                    },
                },
            ],
        )
    """ Step 6: Start instance """
    #print('---Start instance')
    print('<font size=1 face=verdana color=blue>---Start instance</font><br>')
    instance.start()
    try:
        waiter_instance_running.wait(
            InstanceIds=[
                instance_id,
            ]
        )
    except botocore.exceptions.WaiterError as e:
        sys.exit('ERROR: {}'.format(e))
    
    """ Step 7: Clean up """
    #print('---Clean up resources')
    print('<font size=1 face=verdana color=blue>---Clean up resources</font><br>')
    for cleanup in volume_data:
        #print('---Remove snapshot {}'.format(cleanup['snapshot'].id))
        print('<font size=1 face=verdana color=blue>---Remove snapshot <b><font size=1 color=red> {}</font></b></font><br>'.format(cleanup['snapshot'].id))
        cleanup['snapshot'].delete()
        #print('---Remove encrypted snapshot {}'.format(cleanup['snapshot_encrypted'].id))
        print('<font size=1 face=verdana color=blue>---Remove encrypted snapshot <b><font size=1 color=red> {}</font></b></font><br>'.format(cleanup['snapshot_encrypted'].id))
        cleanup['snapshot_encrypted'].delete()
        #print('---Remove original volume {}'.format(cleanup['volume'].id))
        print('<font size=1 face=verdana color=blue>---Remove original volume <b><font size=1 color=red> {}</font></b></font><br>'.format(cleanup['volume'].id))
        cleanup['volume'].delete()
    
    #print('Encryption finished')
    print('<font size=1 face=verdana color=blue>Encryption finished</font><br>')

if __name__ == "__main__":
    main(sys.argv[1:])

Output of the Script

Output in HTML format

Takeaway

Encrypting EBS volumes is easy with this Python script leveraging AWS APIs. Follow the steps above to improve security and meet compliance requirements. Let me know if you have any other questions!

Hope this helps. Happy reading !!!

~Anand M



Leave a comment

About Me

I’m a Hands-On Technical & Entrprise Solutions Architect based out of Houston, TX. I have been working on Oracle ERP, Oracle Database and Cloud technologies for over 20 years and still going strong for learning new things.

You can connect me on Linkedin and also reach out to me

I am certified for 8x AWS, OCP (Oracle Certified Professionals), PMP, ITTL and 6 Sigma.

Disclaimer

This is a personal blog. Any views or opinions represented in this blog are personal and belong solely to the blog owner and do not represent those of people, institutions or organizations that the owner may or may not be associated with in professional or personal capacity, unless explicitly stated.
All content provided on this blog is for informational purposes only. The owner of this blog makes no representations as to the accuracy or completeness of any information on this site or found by following any link on this site.

The owner will not be liable for any errors or omissions in this information nor for the availability of this information. The owner will not be liable for any losses, injuries, or damages from the display or use of this information. Any script available on the blog post MUST be tested before they are run against Production environment.

Newsletter