Skip to content

Commit

Permalink
feat(secretsmanager): import secrets by name (#10309)
Browse files Browse the repository at this point in the history
Adds the ability to import secrets by name, including without the SecretsManager
assigned suffix. As long as a secret with the same name has been created in each
region with the same name, this allows for the same `fromSecretName` usage in
stacks across regions.

Oddly enough, most CloudFormation templates that take references to secrets
accept either the full-form ARN, including the suffix or just the base secret
name (not in ARN format). The one place where a full ARN format is needed is in
IAM policy statements, where the wildcard is necessary to account for the
suffix.

Tested this manually against an existing secret with a CodeBuild project; per
the CloudFormation docs, this should work equally well with other
SecretsManager-integrated services.

fixes #7444
fixes #7949
fixes #7994
  • Loading branch information
njlynch committed Sep 14, 2020
1 parent f5c5d06 commit a8e8ed3
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 18 deletions.
5 changes: 5 additions & 0 deletions packages/@aws-cdk/aws-docdb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
},
"awslint": {
"exclude": [
"attribute-tag:@aws-cdk/aws-docdb.DatabaseSecret.secretName"
]
},
"stability": "experimental",
"maturity": "experimental",
"awscdkio": {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
},
"awslint": {
"exclude": [
"attribute-tag:@aws-cdk/aws-rds.DatabaseSecret.secretName",
"props-physical-name:@aws-cdk/aws-rds.ParameterGroupProps",
"props-physical-name:@aws-cdk/aws-rds.DatabaseClusterProps",
"props-physical-name:@aws-cdk/aws-rds.DatabaseClusterFromSnapshotProps",
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-redshift/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
},
"awslint": {
"exclude": [
"attribute-tag:@aws-cdk/aws-redshift.DatabaseSecret.secretName",
"docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterName",
"docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterValue",
"props-physical-name:@aws-cdk/aws-redshift.ClusterParameterGroupProps",
Expand Down
40 changes: 36 additions & 4 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,6 +15,7 @@ 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:

Expand Down Expand Up @@ -55,18 +57,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,13 +82,16 @@ 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
new secretsmanager.SecretRotation(this, 'SecretRotation', {
application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, // MySQL single user scheme
secret: mySecret,
target: myDatabase, // a Connectable
vpc: myVpc, // The VPC where the secret rotation application will be deployed
Expand All @@ -91,6 +100,7 @@ new SecretRotation(this, 'SecretRotation', {
```

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

```json
{
"engine": "<required: database engine>",
Expand All @@ -104,9 +114,10 @@ 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,
new secretsmanager.SecretRotation(stack, 'SecretRotation', {
application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_MULTI_USER,
secret: myUserSecret, // The secret that will be rotated
masterSecret: myMasterSecret, // The secret used for the rotation
target: myDatabase,
Expand All @@ -116,3 +127,24 @@ new SecretRotation(stack, 'SecretRotation', {

See also [aws-rds](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-rds/README.md) where
credentials generation and rotation is integrated.

### Importing Secrets

Existing secrets can be imported by ARN, name, and other attributes (including the KMS key used to encrypt the secret).
Secrets imported by name can used the short-form of the name (without the SecretsManager-provided suffx);
the secret name must exist in the same account and region as the stack.
Importing by name makes it easier to reference secrets created in different regions, each with their own suffix and ARN.

```ts
import * as kms from '@aws-cdk/aws-kms';

const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9';
const encryptionKey = kms.Key.fromKeyArn(stack, 'MyEncKey', 'arn:aws:kms:eu-west-1:111111111111:key/21c4b39b-fde2-4273-9ac0-d9bb5c0d0030');
const mySecretFromArn = secretsmanager.Secret.fromSecretArn(stack, 'SecretFromArn', secretArn);
const mySecretFromName = secretsmanager.Secret.fromSecretName(stack, 'SecretFromName', 'MySecret') // Note: the -f3gDy9 suffix is optional
const mySecretFromAttrs = secretsmanager.Secret.fromSecretAttributes(stack, 'SecretFromAttributes', {
secretArn,
encryptionKey,
secretName: 'MySecret', // Optional, will be calculated from the ARN
});
```
76 changes: 68 additions & 8 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { Construct, IResource, RemovalPolicy, Resource, SecretValue, Stack } from '@aws-cdk/core';
import { Construct, IConstruct, IResource, RemovalPolicy, Resource, SecretValue, Stack } from '@aws-cdk/core';
import { ResourcePolicy } from './policy';
import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule';
import * as secretsmanager from './secretsmanager.generated';
Expand All @@ -21,6 +21,11 @@ export interface ISecret extends IResource {
*/
readonly secretArn: string;

/**
* The name of the secret
*/
readonly secretName: string;

/**
* Retrieve the value of the stored secret as a `SecretValue`.
* @attribute
Expand Down Expand Up @@ -124,6 +129,13 @@ export interface SecretAttributes {
* The ARN of the secret in SecretsManager.
*/
readonly secretArn: string;

/**
* The name of the secret in SecretsManager.
*
* @default - the name is derived from the secretArn.
*/
readonly secretName?: string;
}

/**
Expand All @@ -132,18 +144,19 @@ export interface SecretAttributes {
abstract class SecretBase extends Resource implements ISecret {
public abstract readonly encryptionKey?: kms.IKey;
public abstract readonly secretArn: string;
public abstract readonly secretName: string;

protected abstract readonly autoCreatePolicy: boolean;

private policy?: ResourcePolicy;

public grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant {
// @see https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html
// @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html

const result = iam.Grant.addToPrincipal({
grantee,
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
resourceArns: [this.secretArn],
resourceArns: [this.arnForPolicies],
scope: this,
});
if (versionStages != null && result.principalStatement) {
Expand All @@ -153,7 +166,7 @@ abstract class SecretBase extends Resource implements ISecret {
}

if (this.encryptionKey) {
// @see https://docs.aws.amazon.com/fr_fr/kms/latest/developerguide/services-secrets-manager.html
// @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html
this.encryptionKey.grantDecrypt(
new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, grantee.grantPrincipal),
);
Expand All @@ -167,7 +180,7 @@ abstract class SecretBase extends Resource implements ISecret {
const result = iam.Grant.addToPrincipal({
grantee,
actions: ['secretsmanager:PutSecretValue', 'secretsmanager:UpdateSecret'],
resourceArns: [this.secretArn],
resourceArns: [this.arnForPolicies],
scope: this,
});

Expand Down Expand Up @@ -222,6 +235,12 @@ abstract class SecretBase extends Resource implements ISecret {
principals: [new iam.AccountRootPrincipal()],
}));
}

/**
* Provides an identifier for this secret for use in IAM policies. Typically, this is just the secret ARN.
* However, secrets imported by name require a different format.
*/
protected get arnForPolicies() { return this.secretArn; }
}

/**
Expand All @@ -233,6 +252,29 @@ export class Secret extends SecretBase {
return Secret.fromSecretAttributes(scope, id, { secretArn });
}

/**
* Imports a secret by secret name; the ARN of the Secret will be set to the secret name.
* A secret with this name must exist in the same account & region.
*/
public static fromSecretName(scope: Construct, id: string, secretName: string): ISecret {
return new class extends SecretBase {
public readonly encryptionKey = undefined;
public readonly secretArn = secretName;
public readonly secretName = secretName;
protected readonly autoCreatePolicy = false;
// Overrides the secretArn for grant* methods, where the secretArn must be in ARN format.
// Also adds a wildcard to the resource name to support the SecretsManager-provided suffix.
protected get arnForPolicies() {
return Stack.of(this).formatArn({
service: 'secretsmanager',
resource: 'secret',
resourceName: this.secretName + '*',
sep: ':',
});
}
}(scope, id);
}

/**
* Import an existing secret into the Stack.
*
Expand All @@ -244,6 +286,7 @@ export class Secret extends SecretBase {
class Import extends SecretBase {
public readonly encryptionKey = attrs.encryptionKey;
public readonly secretArn = attrs.secretArn;
public readonly secretName = parseSecretName(scope, attrs.secretArn, attrs.secretName);
protected readonly autoCreatePolicy = false;
}

Expand All @@ -252,6 +295,7 @@ export class Secret extends SecretBase {

public readonly encryptionKey?: kms.IKey;
public readonly secretArn: string;
public readonly secretName: string;

protected readonly autoCreatePolicy = true;

Expand Down Expand Up @@ -285,12 +329,13 @@ export class Secret extends SecretBase {
});

this.encryptionKey = props.encryptionKey;
this.secretName = this.physicalName;

// @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html#asm-authz
const principle =
const principal =
new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, new iam.AccountPrincipal(Stack.of(this).account));
this.encryptionKey?.grantEncryptDecrypt(principle);
this.encryptionKey?.grant(principle, 'kms:CreateGrant', 'kms:DescribeKey');
this.encryptionKey?.grantEncryptDecrypt(principal);
this.encryptionKey?.grant(principal, 'kms:CreateGrant', 'kms:DescribeKey');
}

/**
Expand Down Expand Up @@ -443,6 +488,7 @@ export class SecretTargetAttachment extends SecretBase implements ISecretTargetA
public encryptionKey?: kms.IKey | undefined;
public secretArn = secretTargetAttachmentSecretArn;
public secretTargetAttachmentSecretArn = secretTargetAttachmentSecretArn;
public secretName = parseSecretName(scope, secretTargetAttachmentSecretArn);
protected readonly autoCreatePolicy = false;
}

Expand All @@ -451,6 +497,7 @@ export class SecretTargetAttachment extends SecretBase implements ISecretTargetA

public readonly encryptionKey?: kms.IKey;
public readonly secretArn: string;
public readonly secretName: string;

/**
* @attribute
Expand All @@ -469,6 +516,7 @@ export class SecretTargetAttachment extends SecretBase implements ISecretTargetA
});

this.encryptionKey = props.secret.encryptionKey;
this.secretName = props.secret.secretName;

// This allows to reference the secret after attachment (dependency).
this.secretArn = attachment.ref;
Expand Down Expand Up @@ -552,3 +600,15 @@ export interface SecretStringGenerator {
*/
readonly generateStringKey?: string;
}

/** Returns the secret name if defined, otherwise attempts to parse it from the ARN. */
export function parseSecretName(construct: IConstruct, secretArn: string, secretName?: string) {
if (secretName) { return secretName; }
const resourceName = Stack.of(construct).parseArn(secretArn).resourceName;
if (resourceName) {

This comment has been minimized.

Copy link
@rrrix

rrrix Sep 25, 2020

I believe this is assuming the secretArn is a real value, not a token (which doesn't exist yet). If it doesn't exist yet, there is no resourceName.

// Secret resource names are in the format `${secretName}-${SecretsManager suffix}`
const secretNameFromArn = resourceName.substr(0, resourceName.lastIndexOf('-'));
if (secretNameFromArn) { return secretNameFromArn; }
}
throw new Error('invalid ARN format; no secret name provided');
}
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 @@ -95,6 +95,7 @@
},
"awslint": {
"exclude": [
"attribute-tag:@aws-cdk/aws-secretsmanager.Secret.secretName",
"from-signature:@aws-cdk/aws-secretsmanager.SecretTargetAttachment.fromSecretTargetAttachmentSecretArn",
"from-attributes:fromSecretTargetAttachmentAttributes",
"props-physical-name:@aws-cdk/aws-secretsmanager.RotationScheduleProps",
Expand Down
Loading

0 comments on commit a8e8ed3

Please sign in to comment.