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): import secrets by name #10309

Merged
merged 4 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
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"
njlynch marked this conversation as resolved.
Show resolved Hide resolved
]
},
"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
});
```
73 changes: 65 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;
njlynch marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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 arnForPolicies() { return this.secretArn; }
njlynch marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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;
njlynch marked this conversation as resolved.
Show resolved Hide resolved
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 arnForPolicies() {
return Stack.of(this).formatArn({
service: 'secretsmanager',
resource: 'secret',
resourceName: this.secretName + '*',
njlynch marked this conversation as resolved.
Show resolved Hide resolved
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,12 @@ 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) {
const secretNameFromNameOrArn = secretName ?? Stack.of(construct).parseArn(secretArn).resourceName;
njlynch marked this conversation as resolved.
Show resolved Hide resolved
if (!secretNameFromNameOrArn) {
throw new Error('invalid ARN format; no secret name provided');
}
return secretNameFromNameOrArn;
}
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