Skip to content

Commit

Permalink
feat(secretsmanager): Specify secret value at creation (#9594)
Browse files Browse the repository at this point in the history
Enables customers to supply their own secret value in the cases where an auto-
generated value is not viable. This exposes the secret value in the cdk output,
and CloudFormation template, but not CloudWatch/CloudTrail.

fixes #5810

----

**PR Notes:**
1. Any feedback / thoughts on how else (besides the docstring) to warn folks of the implications of this approach?
2. The secret string can either be a plain string or string representation of a JSON value. I briefly toyed with creating `secretString` and `secretValueJson` or something, and only allowing one or the other, but wasn't sure it was better. Suggestions on this interface welcome.

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
njlynch authored Aug 11, 2020
1 parent 979558e commit 07fedff
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 7 deletions.
42 changes: 36 additions & 6 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## AWS Secrets Manager Construct Library

<!--BEGIN STABILITY BANNER-->
---

Expand All @@ -14,15 +15,35 @@ import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
```

### Create a new Secret in a Stack

In order to have SecretsManager generate a new secret value automatically,
you can get started with the following:

[example of creating a secret](test/integ.secret.lit.ts)
```ts
// Default secret
const secret = new secretsmanager.Secret(this, 'Secret');

// Using the default secret
new iam.User(this, 'User', {
password: secret.secretValue,
});

// Templated secret
const templatedSecret = new secretsmanager.Secret(this, 'TemplatedSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'user' }),
generateStringKey: 'password',
},
});

// Using the templated secret
new iam.User(this, 'OtherUser', {
userName: templatedSecret.secretValueFromJson('username').toString(),
password: templatedSecret.secretValueFromJson('password'),
});
```

The `Secret` construct does not allow specifying the `SecretString` property
of the `AWS::SecretsManager::Secret` resource (as this will almost always
lead to the secret being surfaced in plain text and possibly committed to
your source control).
[see also this example of creating a secret](test/integ.secret.lit.ts)

If you need to use a pre-existing secret, the recommended way is to manually
provision the secret in *AWS SecretsManager* and use the `Secret.fromSecretArn`
Expand All @@ -43,7 +64,7 @@ A secret can set `RemovalPolicy`. If it set to `RETAIN`, that removing a secret

### Grant permission to use the secret to a role

You must grant permission to a resource for that resource to be allowed to
You must grant permission to a resource for that resource to be allowed to
use a secret. This can be achieved with the `Secret.grantRead` and/or `Secret.grantUpdate`
method, depending on your need:

Expand All @@ -55,18 +76,22 @@ secret.grantWrite(role);
```

If, as in the following example, your secret was created with a KMS key:

```ts
const key = new kms.Key(stack, 'KMS');
const secret = new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key });
secret.grantRead(role);
secret.grantWrite(role);
```

then `Secret.grantRead` and `Secret.grantWrite` will also grant the role the
relevant encrypt and decrypt permissions to the KMS key through the
SecretsManager service principal.

### Rotating a Secret with a custom Lambda function

A rotation schedule can be added to a Secret using a custom Lambda function:

```ts
const fn = new lambda.Function(...);
const secret = new secretsmanager.Secret(this, 'Secret');
Expand All @@ -76,10 +101,13 @@ secret.addRotationSchedule('RotationSchedule', {
automaticallyAfter: Duration.days(15)
});
```

See [Overview of the Lambda Rotation Function](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html) on how to implement a Lambda Rotation Function.

### Rotating database credentials

Define a `SecretRotation` to rotate database credentials:

```ts
new SecretRotation(this, 'SecretRotation', {
application: SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, // MySQL single user scheme
Expand All @@ -90,6 +118,7 @@ new SecretRotation(this, 'SecretRotation', {
```

The secret must be a JSON string with the following format:

```json
{
"engine": "<required: database engine>",
Expand All @@ -103,6 +132,7 @@ The secret must be a JSON string with the following format:
```

For the multi user scheme, a `masterSecret` must be specified:

```ts
new SecretRotation(stack, 'SecretRotation', {
application: SecretRotationApplication.MYSQL_ROTATION_MULTI_USER,
Expand Down
27 changes: 26 additions & 1 deletion packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ export interface SecretProps {
*/
readonly secretName?: string;

/**
* Secret value (WARNING).
*
* **WARNING:** *It is **highly** encouraged to leave this field undefined and allow SecretsManager to create the secret value.
* The secret string -- if provided -- will be included in the output of the cdk as part of synthesis,
* and will appear in the CloudFormation template in the console*.
*
* Specifies text data that you want to encrypt and store in this new version of the secret.
* May be a simple string value, or a string representation of a JSON structure.
*
* Only one of `secretString` and `generateSecretString` can be provided.
*
* @default - SecretsManager generates a new secret value.
*/
readonly secretString?: string;

/**
* Policy to apply when the secret is removed from this stack.
*
Expand Down Expand Up @@ -266,17 +282,26 @@ export class Secret extends SecretBase {
throw new Error('`secretStringTemplate` and `generateStringKey` must be specified together.');
}

if (props.generateSecretString && props.secretString) {
throw new Error('Cannot specify both `generateSecretString` and `secretString`.');
}

const resource = new secretsmanager.CfnSecret(this, 'Resource', {
description: props.description,
kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn,
generateSecretString: props.generateSecretString || {},
generateSecretString: props.generateSecretString || (props.secretString ? undefined : {}),
name: this.physicalName,
secretString: props.secretString,
});

if (props.removalPolicy) {
resource.applyRemovalPolicy(props.removalPolicy);
}

if (props.secretString) {
this.node.addWarning('Using a `secretString` value which will be visible in plaintext in the CloudFormation template and cdk output.');
}

this.secretArn = this.getResourceArnAttribute(resource.ref, {
service: 'secretsmanager',
resource: 'secret',
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-secretsmanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/assert": "0.0.0",
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@types/nodeunit": "^0.0.31",
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/a
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
import * as secretsmanager from '../lib';
Expand Down Expand Up @@ -574,6 +575,41 @@ export = {
test.done();
},

'can provide a secret value directly'(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const secret = new secretsmanager.Secret(stack, 'Secret', {
secretString: 'mynotsosecretvalue',
});

// THEN
expect(stack).to(haveResource('AWS::SecretsManager::Secret', {
SecretString: 'mynotsosecretvalue',
}));

test.equals(secret.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN);
test.equals(secret.node.metadata[0].data, 'Using a `secretString` value which will be visible in plaintext in the CloudFormation template and cdk output.');

test.done();
},

'throws when specifying secretString and generateStringKey'(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// THEN
test.throws(() => new secretsmanager.Secret(stack, 'Secret', {
generateSecretString: {
excludeCharacters: '@',
},
secretString: 'myexistingsecret',
}), /Cannot specify both `generateSecretString` and `secretString`./);

test.done();
},

'equivalence of SecretValue and Secret.fromSecretAttributes'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
Expand Down

0 comments on commit 07fedff

Please sign in to comment.