Skip to content

Commit

Permalink
feat(secretsmanager): hosted rotation (#10790)
Browse files Browse the repository at this point in the history
Add support for secret rotation using a hosted rotation Lambda function.

This should eventually replace the `SecretRotation` class which uses
a serverless application (old way of doing this, currently used in
`aws-rds`).

Note: the `HostedRotationLambda` CF property doesn't support
excluding characters yet so we'll have to wait until we can use it
in `aws-rds`.

See https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html
See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold committed Oct 13, 2020
1 parent 1fbb8bc commit 2cb8e22
Show file tree
Hide file tree
Showing 6 changed files with 691 additions and 10 deletions.
29 changes: 28 additions & 1 deletion packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ 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
### Rotating a Secret

#### Using a Custom Lambda Function

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

Expand All @@ -85,6 +87,31 @@ secret.addRotationSchedule('RotationSchedule', {

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.

#### Using a Hosted Lambda Function

Use the `hostedRotation` prop to rotate a secret with a hosted Lambda function:

```ts
const secret = new secretsmanager.Secret(this, 'Secret');

secret.addRotationSchedule('RotationSchedule', {
hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(),
});
```

Hosted rotation is available for secrets representing credentials for MySQL, PostgreSQL, Oracle,
MariaDB, SQLServer, Redshift and MongoDB (both for the single and multi user schemes).

When deployed in a VPC, the hosted rotation implements `ec2.IConnectable`:

```ts
const myHostedRotation = secretsmanager.HostedRotation.mysqlSingleUser({ vpc: myVpc });
secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotation });
dbConnections.allowDefaultPortFrom(hostedRotation);
```

See also [Automating secret creation in AWS CloudFormation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html).

### Rotating database credentials

Define a `SecretRotation` to rotate database credentials:
Expand Down
277 changes: 273 additions & 4 deletions packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as ec2 from '@aws-cdk/aws-ec2';
import * as lambda from '@aws-cdk/aws-lambda';
import { Duration, Resource } from '@aws-cdk/core';
import { Duration, Resource, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { ISecret } from './secret';
import { CfnRotationSchedule } from './secretsmanager.generated';
Expand All @@ -9,9 +10,18 @@ import { CfnRotationSchedule } from './secretsmanager.generated';
*/
export interface RotationScheduleOptions {
/**
* The Lambda function that can rotate the secret.
* A Lambda function that can rotate the secret.
*
* @default - either `rotationLambda` or `hostedRotation` must be specified
*/
readonly rotationLambda: lambda.IFunction;
readonly rotationLambda?: lambda.IFunction;

/**
* Hosted rotation
*
* @default - either `rotationLambda` or `hostedRotation` must be specified
*/
readonly hostedRotation?: HostedRotation;

/**
* Specifies the number of days after the previous rotation before
Expand All @@ -28,6 +38,23 @@ export interface RotationScheduleOptions {
export interface RotationScheduleProps extends RotationScheduleOptions {
/**
* The secret to rotate.
*
* If hosted rotation is used, this must be a JSON string with the following format:
*
* ```
* {
* "engine": <required: database engine>,
* "host": <required: instance host name>,
* "username": <required: username>,
* "password": <required: password>,
* "dbname": <optional: database name>,
* "port": <optional: if not specified, default port will be used>,
* "masterarn": <required for multi user rotation: the arn of the master secret which will be used to create users/change passwords>
* }
* ```
*
* This is typically the case for a secret referenced from an `AWS::SecretsManager::SecretTargetAttachment`
* or an `ISecret` returned by the `attach()` method of `Secret`.
*/
readonly secret: ISecret;
}
Expand All @@ -39,12 +66,254 @@ export class RotationSchedule extends Resource {
constructor(scope: Construct, id: string, props: RotationScheduleProps) {
super(scope, id);

if ((!props.rotationLambda && !props.hostedRotation) || (props.rotationLambda && props.hostedRotation)) {
throw new Error('One of `rotationLambda` or `hostedRotation` must be specified.');
}

new CfnRotationSchedule(this, 'Resource', {
secretId: props.secret.secretArn,
rotationLambdaArn: props.rotationLambda.functionArn,
rotationLambdaArn: props.rotationLambda?.functionArn,
hostedRotationLambda: props.hostedRotation?.bind(props.secret, this),
rotationRules: {
automaticallyAfterDays: props.automaticallyAfter && props.automaticallyAfter.toDays() || 30,
},
});

// Prevent secrets deletions when rotation is in place
props.secret.denyAccountRootDelete();
}
}

/**
* Single user hosted rotation options
*/
export interface SingleUserHostedRotationOptions {
/**
* A name for the Lambda created to rotate the secret
*
* @default - a CloudFormation generated name
*/
readonly functionName?: string;

/**
* A list of security groups for the Lambda created to rotate the secret
*
* @default - a new security group is created
*/
readonly securityGroups?: ec2.ISecurityGroup[];

/**
* The VPC where the Lambda rotation function will run.
*
* @default - the Lambda is not deployed in a VPC
*/
readonly vpc?: ec2.IVpc;

/**
* The type of subnets in the VPC where the Lambda rotation function will run.
*
* @default - the Vpc default strategy if not specified.
*/
readonly vpcSubnets?: ec2.SubnetSelection;
}

/**
* Multi user hosted rotation options
*/
export interface MultiUserHostedRotationOptions extends SingleUserHostedRotationOptions {
/**
* The master secret for a multi user rotation scheme
*/
readonly masterSecret: ISecret;
}

/**
* A hosted rotation
*/
export class HostedRotation implements ec2.IConnectable {
/** MySQL Single User */
public static mysqlSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.MYSQL_SINGLE_USER, options);
}

/** MySQL Multi User */
public static mysqlMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.MYSQL_MULTI_USER, options, options.masterSecret);
}

/** PostgreSQL Single User */
public static postgreSqlSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.POSTGRESQL_SINGLE_USER, options);
}

/** PostgreSQL Multi User */
public static postgreSqlMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.POSTGRESQL_MULTI_USER, options, options.masterSecret);
}

/** Oracle Single User */
public static oracleSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.ORACLE_SINGLE_USER, options);
}

/** Oracle Multi User */
public static oracleMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.ORACLE_MULTI_USER, options, options.masterSecret);
}

/** MariaDB Single User */
public static mariaDbSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.MARIADB_SINGLE_USER, options);
}

/** MariaDB Multi User */
public static mariaDbMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.MARIADB_MULTI_USER, options, options.masterSecret);
}

/** SQL Server Single User */
public static sqlServerSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.SQLSERVER_SINGLE_USER, options);
}

/** SQL Server Multi User */
public static sqlServerMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.SQLSERVER_MULTI_USER, options, options.masterSecret);
}

/** Redshift Single User */
public static redshiftSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.REDSHIFT_SINGLE_USER, options);
}

/** Redshift Multi User */
public static redshiftMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.REDSHIFT_MULTI_USER, options, options.masterSecret);
}

/** MongoDB Single User */
public static mongoDbSingleUser(options: SingleUserHostedRotationOptions = {}) {
return new HostedRotation(HostedRotationType.MONGODB_SINGLE_USER, options);
}

/** MongoDB Multi User */
public static mongoDbMultiUser(options: MultiUserHostedRotationOptions) {
return new HostedRotation(HostedRotationType.MONGODB_MULTI_USER, options, options.masterSecret);
}

private _connections?: ec2.Connections;

private constructor(
private readonly type: HostedRotationType,
private readonly props: SingleUserHostedRotationOptions | MultiUserHostedRotationOptions,
private readonly masterSecret?: ISecret,
) {
if (type.isMultiUser && !masterSecret) {
throw new Error('The `masterSecret` must be specified when using the multi user scheme.');
}
}

/**
* Binds this hosted rotation to a secret
*/
public bind(secret: ISecret, scope: Construct): CfnRotationSchedule.HostedRotationLambdaProperty {
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-hostedrotationlambda.html
Stack.of(scope).addTransform('AWS::SecretsManager-2020-07-23');

if (!this.props.vpc && this.props.securityGroups) {
throw new Error('`vpc` must be specified when specifying `securityGroups`.');
}

if (this.props.vpc) {
this._connections = new ec2.Connections({
securityGroups: this.props.securityGroups || [new ec2.SecurityGroup(scope, 'SecurityGroup', {
vpc: this.props.vpc,
})],
});
}

// Prevent master secret deletion when rotation is in place
if (this.masterSecret) {
this.masterSecret.denyAccountRootDelete();
}

return {
rotationType: this.type.name,
kmsKeyArn: secret.encryptionKey?.keyArn,
masterSecretArn: this.masterSecret?.secretArn,
masterSecretKmsKeyArn: this.masterSecret?.encryptionKey?.keyArn,
rotationLambdaName: this.props.functionName,
vpcSecurityGroupIds: this._connections?.securityGroups?.map(s => s.securityGroupId).join(','),
vpcSubnetIds: this.props.vpc?.selectSubnets(this.props.vpcSubnets).subnetIds.join(','),
};
}

/**
* Security group connections for this hosted rotation
*/
public get connections() {
if (!this.props.vpc) {
throw new Error('Cannot use connections for a hosted rotation that is not deployed in a VPC');
}

// If we are in a vpc and bind() has been called _connections should be defined
if (!this._connections) {
throw new Error('Cannot use connections for a hosted rotation that has not been bound to a secret');
}

return this._connections;
}
}

/**
* Hosted rotation type
*/
export class HostedRotationType {
/** MySQL Single User */
public static readonly MYSQL_SINGLE_USER = new HostedRotationType('MySQLSingleUser');

/** MySQL Multi User */
public static readonly MYSQL_MULTI_USER = new HostedRotationType('MySQLMultiUser', true);

/** PostgreSQL Single User */
public static readonly POSTGRESQL_SINGLE_USER = new HostedRotationType('PostgreSQLSingleUser');

/** PostgreSQL Multi User */
public static readonly POSTGRESQL_MULTI_USER = new HostedRotationType('PostgreSQLMultiUser', true);

/** Oracle Single User */
public static readonly ORACLE_SINGLE_USER = new HostedRotationType('OracleSingleUser');

/** Oracle Multi User */
public static readonly ORACLE_MULTI_USER = new HostedRotationType('OracleMultiUser', true);

/** MariaDB Single User */
public static readonly MARIADB_SINGLE_USER = new HostedRotationType('MariaDBSingleUser');

/** MariaDB Multi User */
public static readonly MARIADB_MULTI_USER = new HostedRotationType('MariaDBMultiUser', true);

/** SQL Server Single User */
public static readonly SQLSERVER_SINGLE_USER = new HostedRotationType('SQLServerSingleUser')

/** SQL Server Multi User */
public static readonly SQLSERVER_MULTI_USER = new HostedRotationType('SQLServerMultiUser', true);

/** Redshift Single User */
public static readonly REDSHIFT_SINGLE_USER = new HostedRotationType('RedshiftSingleUser')

/** Redshift Multi User */
public static readonly REDSHIFT_MULTI_USER = new HostedRotationType('RedshiftMultiUser', true);

/** MongoDB Single User */
public static readonly MONGODB_SINGLE_USER = new HostedRotationType('MongoDBSingleUser');

/** MongoDB Multi User */
public static readonly MONGODB_MULTI_USER = new HostedRotationType('MongoDBMultiUser', true);

/**
* @param name The type of rotation
* @param isMultiUser Whether the rotation uses the mutli user scheme
*/
private constructor(public readonly name: string, public readonly isMultiUser?: boolean) {}
}
8 changes: 4 additions & 4 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export class SecretRotationApplication {
export interface SecretRotationProps {
/**
* The secret to rotate. It must be a JSON string with the following format:
*
* ```
* {
* "engine": <required: database engine>,
Expand All @@ -148,8 +149,8 @@ export interface SecretRotationProps {
* }
* ```
*
* This is typically the case for a secret referenced from an
* AWS::SecretsManager::SecretTargetAttachment or an `ISecret` returned by the `attach()` method of `Secret`.
* This is typically the case for a secret referenced from an `AWS::SecretsManager::SecretTargetAttachment`
* or an `ISecret` returned by the `attach()` method of `Secret`.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html
*/
Expand Down Expand Up @@ -270,8 +271,7 @@ export class SecretRotation extends CoreConstruct {
automaticallyAfter: props.automaticallyAfter,
});

// Prevent secrets deletions when rotation is in place
props.secret.denyAccountRootDelete();
// Prevent master secret deletion when rotation is in place
if (props.masterSecret) {
props.masterSecret.denyAccountRootDelete();
}
Expand Down
Loading

0 comments on commit 2cb8e22

Please sign in to comment.