From c46acd5f13442c43d0c2ed339e3091dd46002741 Mon Sep 17 00:00:00 2001 From: Nico Tonnhofer Date: Mon, 31 Jan 2022 23:48:01 +0100 Subject: [PATCH] feat(ecr): add server-side encryption configuration (#16966) fixes #15400 With this request you will be able to configure the encryption of your ECR Repository. Before this patch you need to use a L1-Construct and add it via: Python: ```python repo = ecr.Repository(stack, 'Repo') cfn_repo = repo.node.default_child cfn_repo.encryption_configuration = CfnRepository.EncryptionConfigurationProperty(encryption_type="KMS") ``` Now this becomes: ```python repo = ecr.Repository(stack, 'Repo', encryption_type=ecr.RepositoryEncryption.KMS) ``` When using a KMS Key, the `encryption_type` is set automatically to `KMS`. ```python kms_key = kms.Key(stack, 'Key') ecr.Repository(stack, 'Repo', encryption_key=kms_key) ``` Similar to #15571 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ecr/README.md | 26 +++++++ packages/@aws-cdk/aws-ecr/lib/repository.ts | 72 ++++++++++++++++++ packages/@aws-cdk/aws-ecr/package.json | 2 + .../@aws-cdk/aws-ecr/test/repository.test.ts | 74 +++++++++++++++++++ 4 files changed, 174 insertions(+) diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 193d5de2f0645..6e67349024891 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -79,6 +79,32 @@ You can set tag immutability on images in our repository using the `imageTagMuta new ecr.Repository(this, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); ``` +### Encryption + +By default, Amazon ECR uses server-side encryption with Amazon S3-managed encryption keys which encrypts your data at rest using an AES-256 encryption algorithm. For more control over the encryption for your Amazon ECR repositories, you can use server-side encryption with KMS keys stored in AWS Key Management Service (AWS KMS). Read more about this feature in the [ECR Developer Guide](https://docs.aws.amazon.com/AmazonECR/latest/userguide/encryption-at-rest.html). + +When you use AWS KMS to encrypt your data, you can either use the default AWS managed key, which is managed by Amazon ECR, by specifying `RepositoryEncryption.KMS` in the `encryption` property. Or specify your own customer managed KMS key, by specifying the `encryptionKey` property. + +When `encryptionKey` is set, the `encryption` property must be `KMS` or empty. + +In the case `encryption` is set to `KMS` but no `encryptionKey` is set, an AWS managed KMS key is used. + +```ts +new ecr.Repository(this, 'Repo', { + encryption: ecr.RepositoryEncryption.KMS +}); +``` + +Otherwise, a customer-managed KMS key is used if `encryptionKey` was set and `encryption` was optionally set to `KMS`. + +```ts +import * as kms from '@aws-cdk/aws-kms'; + +new ecr.Repository(this, 'Repo', { + encryptionKey: new kms.Key(this, 'Key'), +}); +``` + ## Automatically clean up repositories You can set life cycle rules to automatically clean up old images from your diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 74df965ff5d58..3d4e44c0776a9 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import { ArnFormat, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { CfnRepository } from './ecr.generated'; @@ -327,6 +328,27 @@ export interface RepositoryProps { */ readonly repositoryName?: string; + /** + * The kind of server-side encryption to apply to this repository. + * + * If you choose KMS, you can specify a KMS key via `encryptionKey`. If + * encryptionKey is not specified, an AWS managed KMS key is used. + * + * @default - `KMS` if `encryptionKey` is specified, or `AES256` otherwise. + */ + readonly encryption?: RepositoryEncryption; + + /** + * External KMS key to use for repository encryption. + * + * The 'encryption' property must be either not specified or set to "KMS". + * An error will be emitted if encryption is set to "AES256". + * + * @default - If encryption is set to `KMS` and this property is undefined, + * an AWS managed KMS key is used. + */ + readonly encryptionKey?: kms.IKey; + /** * Life cycle rules to apply to this registry * @@ -490,6 +512,7 @@ export class Repository extends RepositoryBase { scanOnPush: true, }, imageTagMutability: props.imageTagMutability || undefined, + encryptionConfiguration: this.parseEncryption(props), }); resource.applyRemovalPolicy(props.removalPolicy); @@ -602,6 +625,34 @@ export class Repository extends RepositoryBase { validateAnyRuleLast(ret); return ret; } + + /** + * Set up key properties and return the Repository encryption property from the + * user's configuration. + */ + private parseEncryption(props: RepositoryProps): CfnRepository.EncryptionConfigurationProperty | undefined { + + // default based on whether encryptionKey is specified + const encryptionType = props.encryption ?? (props.encryptionKey ? RepositoryEncryption.KMS : RepositoryEncryption.AES_256); + + // if encryption key is set, encryption must be set to KMS. + if (encryptionType !== RepositoryEncryption.KMS && props.encryptionKey) { + throw new Error(`encryptionKey is specified, so 'encryption' must be set to KMS (value: ${encryptionType.value})`); + } + + if (encryptionType === RepositoryEncryption.AES_256) { + return undefined; + } + + if (encryptionType === RepositoryEncryption.KMS) { + return { + encryptionType: 'KMS', + kmsKey: props.encryptionKey?.keyArn, + }; + } + + throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); + } } function validateAnyRuleLast(rules: LifecycleRule[]) { @@ -664,3 +715,24 @@ export enum TagMutability { IMMUTABLE = 'IMMUTABLE', } + +/** + * Indicates whether server-side encryption is enabled for the object, and whether that encryption is + * from the AWS Key Management Service (AWS KMS) or from Amazon S3 managed encryption (SSE-S3). + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#SysMetadata + */ +export class RepositoryEncryption { + /** + * 'AES256' + */ + public static readonly AES_256 = new RepositoryEncryption('AES256'); + /** + * 'KMS' + */ + public static readonly KMS = new RepositoryEncryption('KMS'); + + /** + * @param value the string value of the encryption + */ + protected constructor(public readonly value: string) { } +} diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index e2b8bccc7c29f..3141f0c8dc6d1 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -93,6 +93,7 @@ "dependencies": { "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -100,6 +101,7 @@ "peerDependencies": { "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-ecr/test/repository.test.ts b/packages/@aws-cdk/aws-ecr/test/repository.test.ts index 470638d89355e..232ad400cdff6 100644 --- a/packages/@aws-cdk/aws-ecr/test/repository.test.ts +++ b/packages/@aws-cdk/aws-ecr/test/repository.test.ts @@ -1,6 +1,7 @@ import { EOL } from 'os'; import { Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as ecr from '../lib'; @@ -363,6 +364,79 @@ describe('repository', () => { expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); }); + test('default encryption configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.AES_256 }); + + // THEN + Template.fromStack(stack).templateMatches({ + Resources: { + Repo02AC86CF: { + Type: 'AWS::ECR::Repository', + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + }); + + test('kms encryption configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.KMS }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', + { + EncryptionConfiguration: { + EncryptionType: 'KMS', + }, + }); + }); + + test('kms encryption with custom kms configuration', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + + // WHEN + const custom_key = new kms.Key(stack, 'Key'); + new ecr.Repository(stack, 'Repo', { encryptionKey: custom_key }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECR::Repository', + { + EncryptionConfiguration: { + EncryptionType: 'KMS', + KmsKey: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + }, + }); + }); + + test('fails if with custom kms key and AES256 as encryption', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const custom_key = new kms.Key(stack, 'Key'); + + // THEN + expect(() => { + new ecr.Repository(stack, 'Repo', { encryption: ecr.RepositoryEncryption.AES_256, encryptionKey: custom_key }); + }).toThrow('encryptionKey is specified, so \'encryption\' must be set to KMS (value: AES256)'); + }); + describe('events', () => { test('onImagePushed without imageTag creates the correct event', () => { const stack = new cdk.Stack();