Skip to content

Commit

Permalink
feat(ecr): add server-side encryption configuration (#16966)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
Wurstnase authored Jan 31, 2022
1 parent b30c3f2 commit c46acd5
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
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
});
```

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
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', () => {
// 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

0 comments on commit c46acd5

Please sign in to comment.