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): Specify secret value at creation #9594

Merged
merged 4 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting this here mucks with our doc generation. Put a caption as the first line. For example:

/**
  * Literal secret value (DANGEROUS!)
  */

* 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