Skip to content

Commit

Permalink
feat(secretsmanager): replicate secrets to multiple regions (aws#14266)
Browse files Browse the repository at this point in the history
Secret replication to multiple regions.

Closes aws#14061


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold authored and john-tipper committed May 10, 2021
1 parent 548c2d4 commit 16f85b3
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 1 deletion.
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',
},
],
});
});

0 comments on commit 16f85b3

Please sign in to comment.