Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ecr): add server-side encryption configuration #16966

Merged
merged 7 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-ecr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Wurstnase marked this conversation as resolved.
Show resolved Hide resolved
});
```

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', {
Wurstnase marked this conversation as resolved.
Show resolved Hide resolved
encryptionKey: new kms.Key(this, 'Key'),
});
```

## Automatically clean up repositories

You can set life cycle rules to automatically clean up old images from your
Expand Down
72 changes: 72 additions & 0 deletions packages/@aws-cdk/aws-ecr/lib/repository.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -490,6 +512,7 @@ export class Repository extends RepositoryBase {
scanOnPush: true,
},
imageTagMutability: props.imageTagMutability || undefined,
encryptionConfiguration: this.parseEncryption(props),
});

resource.applyRemovalPolicy(props.removalPolicy);
Expand Down Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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) { }
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ecr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,15 @@
"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"
},
"homepage": "https://github.com/aws/aws-cdk",
"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"
},
Expand Down
74 changes: 74 additions & 0 deletions packages/@aws-cdk/aws-ecr/test/repository.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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', () => {
Wurstnase marked this conversation as resolved.
Show resolved Hide resolved
// 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();
Expand Down