From ccda76898a024de0862bc20dd544fe40eba2b30d Mon Sep 17 00:00:00 2001 From: Brian Pandola Date: Wed, 14 Oct 2020 07:18:50 -0700 Subject: [PATCH] Add KMS Support to EBS Encrypted Volumes (#3383) * Properly coerce `Encrypted` attribute to bool on request/response. * Create and use a default AWS managed CMK for EBS when clients request an encrypted volume without specifying a KmsKeyId. NOTE: A client-provided KmsKeyId is simply stored as-is, and is not validated against the KMS backend. This is in keeping with other moto backends (RDS, Redshift) that currently also accept unvalidated customer master key (CMK) parameters, but could be an area for future improvement. Closes #3248 --- moto/ec2/exceptions.py | 10 ++++ moto/ec2/models.py | 41 ++++++++++++-- moto/ec2/responses/elastic_block_store.py | 17 ++++-- tests/test_ec2/test_elastic_block_store.py | 63 ++++++++++++++++++++++ 4 files changed, 124 insertions(+), 7 deletions(-) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index b2d7e8aab..e14a60bf1 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -573,3 +573,13 @@ class InvalidLaunchTemplateNameError(EC2ClientError): "InvalidLaunchTemplateName.AlreadyExistsException", "Launch template name already in use.", ) + + +class InvalidParameterDependency(EC2ClientError): + def __init__(self, param, param_needed): + super(InvalidParameterDependency, self).__init__( + "InvalidParameterDependency", + "The parameter [{0}] requires the parameter {1} to be set.".format( + param, param_needed + ), + ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index d1187ac9d..a7a34cbf9 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -28,6 +28,7 @@ from moto.core.utils import ( camelcase_to_underscores, ) from moto.core import ACCOUNT_ID +from moto.kms import kms_backends from .exceptions import ( CidrLimitExceeded, @@ -97,6 +98,7 @@ from .exceptions import ( ResourceAlreadyAssociatedError, RulesPerSecurityGroupLimitExceededError, TagLimitExceeded, + InvalidParameterDependency, ) from .utils import ( EC2_RESOURCE_TO_PREFIX, @@ -2425,7 +2427,14 @@ class VolumeAttachment(CloudFormationModel): class Volume(TaggedEC2Resource, CloudFormationModel): def __init__( - self, ec2_backend, volume_id, size, zone, snapshot_id=None, encrypted=False + self, + ec2_backend, + volume_id, + size, + zone, + snapshot_id=None, + encrypted=False, + kms_key_id=None, ): self.id = volume_id self.size = size @@ -2435,6 +2444,7 @@ class Volume(TaggedEC2Resource, CloudFormationModel): self.snapshot_id = snapshot_id self.ec2_backend = ec2_backend self.encrypted = encrypted + self.kms_key_id = kms_key_id @staticmethod def cloudformation_name_type(): @@ -2548,7 +2558,13 @@ class EBSBackend(object): self.snapshots = {} super(EBSBackend, self).__init__() - def create_volume(self, size, zone_name, snapshot_id=None, encrypted=False): + def create_volume( + self, size, zone_name, snapshot_id=None, encrypted=False, kms_key_id=None + ): + if kms_key_id and not encrypted: + raise InvalidParameterDependency("KmsKeyId", "Encrypted") + if encrypted and not kms_key_id: + kms_key_id = self._get_default_encryption_key() volume_id = random_volume_id() zone = self.get_zone_by_name(zone_name) if snapshot_id: @@ -2557,7 +2573,7 @@ class EBSBackend(object): size = snapshot.volume.size if snapshot.encrypted: encrypted = snapshot.encrypted - volume = Volume(self, volume_id, size, zone, snapshot_id, encrypted) + volume = Volume(self, volume_id, size, zone, snapshot_id, encrypted, kms_key_id) self.volumes[volume_id] = volume return volume @@ -2705,6 +2721,25 @@ class EBSBackend(object): return True + def _get_default_encryption_key(self): + # https://aws.amazon.com/kms/features/#AWS_Service_Integration + # An AWS managed CMK is created automatically when you first create + # an encrypted resource using an AWS service integrated with KMS. + kms = kms_backends[self.region_name] + ebs_alias = "alias/aws/ebs" + if not kms.alias_exists(ebs_alias): + key = kms.create_key( + policy="", + key_usage="ENCRYPT_DECRYPT", + customer_master_key_spec="SYMMETRIC_DEFAULT", + description="Default master key that protects my EBS volumes when no other key is defined", + tags=None, + region=self.region_name, + ) + kms.add_alias(key.id, ebs_alias) + ebs_key = kms.describe_key(ebs_alias) + return ebs_key.arn + class VPC(TaggedEC2Resource, CloudFormationModel): def __init__( diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index 853af936d..fd237e2e4 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -46,9 +46,12 @@ class ElasticBlockStore(BaseResponse): snapshot_id = self._get_param("SnapshotId") tags = self._parse_tag_specification("TagSpecification") volume_tags = tags.get("volume", {}) - encrypted = self._get_param("Encrypted", if_none=False) + encrypted = self._get_bool_param("Encrypted", if_none=False) + kms_key_id = self._get_param("KmsKeyId") if self.is_not_dryrun("CreateVolume"): - volume = self.ec2_backend.create_volume(size, zone, snapshot_id, encrypted) + volume = self.ec2_backend.create_volume( + size, zone, snapshot_id, encrypted, kms_key_id + ) volume.add_tags(volume_tags) template = self.response_template(CREATE_VOLUME_RESPONSE) return template.render(volume=volume) @@ -161,7 +164,10 @@ CREATE_VOLUME_RESPONSE = """