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(secretsmanager): replicate secrets to multiple regions #14266

Merged
merged 2 commits into from Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Expand Up @@ -180,3 +180,28 @@ const mySecretFromAttrs = secretsmanager.Secret.fromSecretAttributes(stack, 'Sec
encryptionKey,
});
```

## Replicating secrets

Secrets can be replicated to multiple regions by specifying `replicaRegions`:

```ts
new secretsmanager.Secret(this, 'Secret', {
replicaRegions: [
{
region: 'eu-west-1',
},
{
region: 'eu-central-1',
encryptionKey: myKey,
}
]
});
```

Alternatively, use `addReplicaRegion()`:

```ts
const secret = new secretsmanager.Secret(this, 'Secret');
secret.addReplicaRegion('eu-west-1');
```
51 changes: 50 additions & 1 deletion packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
@@ -1,6 +1,6 @@
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { FeatureFlags, Fn, IResource, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core';
import { FeatureFlags, Fn, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { IConstruct, Construct } from 'constructs';
import { ResourcePolicy } from './policy';
Expand Down Expand Up @@ -134,6 +134,30 @@ export interface SecretProps {
* @default - Not set.
*/
readonly removalPolicy?: RemovalPolicy;

/**
* A list of regions where to replicate this secret.
*
* @default - Secret is not replicated
*/
readonly replicaRegions?: ReplicaRegion[];
}

/**
* Secret replica region
*/
export interface ReplicaRegion {
/**
* The name of the region
*/
readonly region: string;

/**
* The customer-managed encryption key to use for encrypting the secret value.
*
* @default - A default KMS key for the account and region is used.
*/
readonly encryptionKey?: kms.IKey;
}

/**
Expand Down Expand Up @@ -408,6 +432,8 @@ export class Secret extends SecretBase {
public readonly secretArn: string;
public readonly secretName: string;

private replicaRegions: secretsmanager.CfnSecret.ReplicaRegionProperty[] = [];

protected readonly autoCreatePolicy = true;

constructor(scope: Construct, id: string, props: SecretProps = {}) {
Expand All @@ -426,6 +452,7 @@ export class Secret extends SecretBase {
kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn,
generateSecretString: props.generateSecretString || {},
name: this.physicalName,
replicaRegions: Lazy.any({ produce: () => this.replicaRegions }, { omitEmptyArray: true }),
});

if (props.removalPolicy) {
Expand All @@ -450,6 +477,10 @@ export class Secret extends SecretBase {
new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, new iam.AccountPrincipal(Stack.of(this).account));
this.encryptionKey?.grantEncryptDecrypt(principal);
this.encryptionKey?.grant(principal, 'kms:CreateGrant', 'kms:DescribeKey');

for (const replica of props.replicaRegions ?? []) {
this.addReplicaRegion(replica.region, replica.encryptionKey);
}
}

/**
Expand All @@ -465,6 +496,24 @@ export class Secret extends SecretBase {
...options,
});
}

/**
* Adds a replica region for the secret
*
* @param region The name of the region
* @param encryptionKey The customer-managed encryption key to use for encrypting the secret value.
*/
public addReplicaRegion(region: string, encryptionKey?: kms.IKey): void {
const stack = Stack.of(this);
if (!Token.isUnresolved(stack.region) && !Token.isUnresolved(region) && region === stack.region) {
throw new Error('Cannot add the region where this stack is deployed as a replica region.');
}

this.replicaRegions.push({
region,
kmsKeyId: encryptionKey?.keyArn,
});
}
}

/**
Expand Down
@@ -0,0 +1,15 @@
{
"Resources": {
"SecretA720EF05": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"GenerateSecretString": {},
"ReplicaRegions": [
{
"Region": "eu-central-1"
}
]
}
}
}
}
15 changes: 15 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.replica.ts
@@ -0,0 +1,15 @@
import * as cdk from '@aws-cdk/core';
import * as secretsmanager from '../lib';

class TestStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);

const secret = new secretsmanager.Secret(this, 'Secret');
secret.addReplicaRegion('eu-central-1');
}
}

const app = new cdk.App();
new TestStack(app, 'cdk-integ-secrets-replica');
app.synth();
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts
Expand Up @@ -1071,3 +1071,28 @@ test('fails if secret policy has no IAM principals', () => {
// THEN
expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/);
});

test('with replication regions', () => {
// WHEN
const secret = new secretsmanager.Secret(stack, 'Secret', {
replicaRegions: [
{
region: 'eu-west-1',
},
],
});
secret.addReplicaRegion('eu-central-1', kms.Key.fromKeyArn(stack, 'Key', 'arn:aws:kms:eu-central-1:123456789012:key/my-key-id'));

// THEN
expect(stack).toHaveResource('AWS::SecretsManager::Secret', {
ReplicaRegions: [
{
Region: 'eu-west-1',
},
{
KmsKeyId: 'arn:aws:kms:eu-central-1:123456789012:key/my-key-id',
Region: 'eu-central-1',
},
],
});
});