From 07fedffadf3900d754b5df5a24cc84622299ede4 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Tue, 11 Aug 2020 18:14:29 +0100 Subject: [PATCH] feat(secretsmanager): Specify secret value at creation (#9594) 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* --- .../@aws-cdk/aws-secretsmanager/README.md | 42 ++++++++++++++++--- .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 27 +++++++++++- .../@aws-cdk/aws-secretsmanager/package.json | 1 + .../aws-secretsmanager/test/test.secret.ts | 36 ++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 540cc9a7fa0be..76efe357b9467 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -1,4 +1,5 @@ ## AWS Secrets Manager Construct Library + --- @@ -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` @@ -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: @@ -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'); @@ -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 @@ -90,6 +118,7 @@ new SecretRotation(this, 'SecretRotation', { ``` The secret must be a JSON string with the following format: + ```json { "engine": "", @@ -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, diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 4ca105f58d59a..e4438ec308b7b 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -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. * @@ -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', diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index ca00bf77825cb..b80ecfef3fbc3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -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", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 0a295747e7e5f..e1c5bee53dd7e 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -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'; @@ -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();