Skip to content

Commit

Permalink
feat(rds): more extensive secret rotation support (#5281)
Browse files Browse the repository at this point in the history
Add support for Redshift clusters, DocumentDB databases and the multi user rotation scheme.

Move `SecretRotation` from `aws-rds` to `aws-secretsmanager`.

Add resource policy for secrets and use it to prevent deletion of secrets for which rotation is
enabled.

Update instance class to `t3` in `aws-rds` integration tests (`t2` is being deprecated and Oracle
`t2` instances cannot be created anymore).

Closes #5194

BREAKING CHANGE: `addRotationSingleUser(id: string, options: SecretRotationOptions)` is now `addRotationSingleUser(automaticallyAfter?: Duration)`
  • Loading branch information
jogold authored and rix0rrr committed Dec 20, 2019
1 parent ac748c1 commit b700b77
Show file tree
Hide file tree
Showing 24 changed files with 1,174 additions and 735 deletions.
40 changes: 22 additions & 18 deletions packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,32 +108,36 @@ For an instance database:
const address = instance.instanceEndpoint.socketAddress; // "HOSTNAME:PORT"
```

### Rotating master password
### Rotating credentials
When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically:
```ts
instance.addRotationSingleUser(); // Will rotate automatically after 30 days
```

[example of setting up master password rotation for a cluster](test/integ.cluster-rotation.lit.ts)

Rotation of the master password is also supported for an existing cluster:
The multi user rotation scheme is also available:
```ts
new SecretRotation(stack, 'Rotation', {
secret: importedSecret,
application: SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER
target: importedCluster, // or importedInstance
vpc: importedVpc,
})
instance.addRotationMultiUser('MyUser', {
secret: myImportedSecret
});
```

The `importedSecret` must be a JSON string with the following format:
```json
{
"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>"
}
It's also possible to create user credentials together with the instance/cluster and add rotation:
```ts
const myUserSecret = new rds.DatabaseSecret(this, 'MyUserSecret', {
username: 'myuser'
});
const myUserSecretAttached = myUserSecret.attach(instance); // Adds DB connections information in the secret

instance.addRotationMultiUser('MyUser', { // Add rotation using the multi user scheme
secret: myUserSecretAttached
});
```
**Note**: This user must be created manually in the database using the master credentials.
The rotation will start as soon as this user exists.

See also [@aws-cdk/aws-secretsmanager](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-secretsmanager/README.md) for credentials rotation of existing clusters/instances.

### Metrics
Database instances expose metrics (`cloudwatch.Metric`):
Expand Down
56 changes: 40 additions & 16 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref';
import { DatabaseSecret } from './database-secret';
import { Endpoint } from './endpoint';
import { IParameterGroup } from './parameter-group';
import { BackupProps, DatabaseClusterEngine, InstanceProps, Login } from './props';
import { BackupProps, DatabaseClusterEngine, InstanceProps, Login, RotationMultiUserOptions } from './props';
import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated';
import { SecretRotation, SecretRotationApplication, SecretRotationOptions } from './secret-rotation';

/**
* Properties for a new database cluster
Expand Down Expand Up @@ -190,7 +189,7 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster
public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps {
return {
targetId: this.clusterIdentifier,
targetType: secretsmanager.AttachmentTargetType.CLUSTER
targetType: secretsmanager.AttachmentTargetType.RDS_DB_CLUSTER
};
}
}
Expand Down Expand Up @@ -262,10 +261,8 @@ export class DatabaseCluster extends DatabaseClusterBase {
*/
public readonly secret?: secretsmanager.ISecret;

/**
* The database engine of this cluster
*/
private readonly secretRotationApplication: SecretRotationApplication;
private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication;
private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication;

/**
* The VPC where the DB subnet group is created.
Expand Down Expand Up @@ -302,15 +299,16 @@ export class DatabaseCluster extends DatabaseClusterBase {
});
this.securityGroupId = securityGroup.securityGroupId;

let secret;
let secret: DatabaseSecret | undefined;
if (!props.masterUser.password) {
secret = new DatabaseSecret(this, 'Secret', {
username: props.masterUser.username,
encryptionKey: props.masterUser.kmsKey
});
}

this.secretRotationApplication = props.engine.secretRotationApplication;
this.singleUserRotationApplication = props.engine.singleUserRotationApplication;
this.multiUserRotationApplication = props.engine.multiUserRotationApplication;

const cluster = new CfnDBCluster(this, 'Resource', {
// Basic
Expand Down Expand Up @@ -349,9 +347,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpointAddress, portAttribute);

if (secret) {
this.secret = secret.addTargetAttachment('AttachedSecret', {
target: this
});
this.secret = secret.attach(this);
}

const instanceCount = props.instances != null ? props.instances : 2;
Expand Down Expand Up @@ -415,18 +411,46 @@ export class DatabaseCluster extends DatabaseClusterBase {

/**
* Adds the single user rotation of the master password to this cluster.
*
* @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation
* before Secrets Manager triggers the next automatic rotation.
*/
public addRotationSingleUser(id: string, options: SecretRotationOptions = {}): SecretRotation {
public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation {
if (!this.secret) {
throw new Error('Cannot add single user rotation for a cluster without secret.');
}
return new SecretRotation(this, id, {

const id = 'RotationSingleUser';
const existing = this.node.tryFindChild(id);
if (existing) {
throw new Error('A single user rotation was already added to this cluster.');
}

return new secretsmanager.SecretRotation(this, id, {
secret: this.secret,
application: this.secretRotationApplication,
automaticallyAfter,
application: this.singleUserRotationApplication,
vpc: this.vpc,
vpcSubnets: this.vpcSubnets,
target: this,
});
}

/**
* Adds the multi user rotation to this cluster.
*/
public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation {
if (!this.secret) {
throw new Error('Cannot add multi user rotation for a cluster without secret.');
}
return new secretsmanager.SecretRotation(this, id, {
secret: options.secret,
masterSecret: this.secret,
automaticallyAfter: options.automaticallyAfter,
application: this.multiUserRotationApplication,
vpc: this.vpc,
vpcSubnets: this.vpcSubnets,
target: this,
...options
});
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-rds/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from './cluster';
export * from './cluster-ref';
export * from './props';
export * from './parameter-group';
export * from './secret-rotation';
export * from './database-secret';
export * from './endpoint';
export * from './option-group';
Expand Down
81 changes: 54 additions & 27 deletions packages/@aws-cdk/aws-rds/lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import { DatabaseSecret } from './database-secret';
import { Endpoint } from './endpoint';
import { IOptionGroup } from './option-group';
import { IParameterGroup } from './parameter-group';
import { DatabaseClusterEngine } from './props';
import { DatabaseClusterEngine, RotationMultiUserOptions } from './props';
import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated';
import { SecretRotation, SecretRotationApplication, SecretRotationOptions } from './secret-rotation';

/**
* A database instance
Expand Down Expand Up @@ -144,7 +143,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps {
return {
targetId: this.instanceIdentifier,
targetType: secretsmanager.AttachmentTargetType.INSTANCE
targetType: secretsmanager.AttachmentTargetType.RDS_DB_INSTANCE
};
}
}
Expand All @@ -154,17 +153,19 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
* secret rotation.
*/
export class DatabaseInstanceEngine extends DatabaseClusterEngine {
public static readonly MARIADB = new DatabaseInstanceEngine('mariadb', SecretRotationApplication.MARIADB_ROTATION_SINGLE_USER);
public static readonly MYSQL = new DatabaseInstanceEngine('mysql', SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER);
public static readonly ORACLE_EE = new DatabaseInstanceEngine('oracle-ee', SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER);
public static readonly ORACLE_SE2 = new DatabaseInstanceEngine('oracle-se2', SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER);
public static readonly ORACLE_SE1 = new DatabaseInstanceEngine('oracle-se1', SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER);
public static readonly ORACLE_SE = new DatabaseInstanceEngine('oracle-se', SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER);
public static readonly POSTGRES = new DatabaseInstanceEngine('postgres', SecretRotationApplication.POSTGRES_ROTATION_SINGLE_USER);
public static readonly SQL_SERVER_EE = new DatabaseInstanceEngine('sqlserver-ee', SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER);
public static readonly SQL_SERVER_SE = new DatabaseInstanceEngine('sqlserver-se', SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER);
public static readonly SQL_SERVER_EX = new DatabaseInstanceEngine('sqlserver-ex', SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER);
public static readonly SQL_SERVER_WEB = new DatabaseInstanceEngine('sqlserver-web', SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER);
/* tslint:disable max-line-length */
public static readonly MARIADB = new DatabaseInstanceEngine('mariadb', secretsmanager.SecretRotationApplication.MARIADB_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.MARIADB_ROTATION_MULTI_USER);
public static readonly MYSQL = new DatabaseInstanceEngine('mysql', secretsmanager.SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.MYSQL_ROTATION_MULTI_USER);
public static readonly ORACLE_EE = new DatabaseInstanceEngine('oracle-ee', secretsmanager.SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.ORACLE_ROTATION_MULTI_USER);
public static readonly ORACLE_SE2 = new DatabaseInstanceEngine('oracle-se2', secretsmanager.SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.ORACLE_ROTATION_MULTI_USER);
public static readonly ORACLE_SE1 = new DatabaseInstanceEngine('oracle-se1', secretsmanager.SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.ORACLE_ROTATION_MULTI_USER);
public static readonly ORACLE_SE = new DatabaseInstanceEngine('oracle-se', secretsmanager.SecretRotationApplication.ORACLE_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.ORACLE_ROTATION_MULTI_USER);
public static readonly POSTGRES = new DatabaseInstanceEngine('postgres', secretsmanager.SecretRotationApplication.POSTGRES_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.POSTGRES_ROTATION_MULTI_USER);
public static readonly SQL_SERVER_EE = new DatabaseInstanceEngine('sqlserver-ee', secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_MULTI_USER);
public static readonly SQL_SERVER_SE = new DatabaseInstanceEngine('sqlserver-se', secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_MULTI_USER);
public static readonly SQL_SERVER_EX = new DatabaseInstanceEngine('sqlserver-ex', secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_MULTI_USER);
public static readonly SQL_SERVER_WEB = new DatabaseInstanceEngine('sqlserver-web', secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER, secretsmanager.SecretRotationApplication.SQLSERVER_ROTATION_MULTI_USER);
/* tslint:enable max-line-length */
}

/**
Expand Down Expand Up @@ -665,12 +666,14 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa

protected readonly sourceCfnProps: CfnDBInstanceProps;

private readonly secretRotationApplication: SecretRotationApplication;
private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication;
private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication;

constructor(scope: Construct, id: string, props: DatabaseInstanceSourceProps) {
super(scope, id, props);

this.secretRotationApplication = props.engine.secretRotationApplication;
this.singleUserRotationApplication = props.engine.singleUserRotationApplication;
this.multiUserRotationApplication = props.engine.multiUserRotationApplication;

this.sourceCfnProps = {
...this.newCfnProps,
Expand All @@ -687,18 +690,46 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa

/**
* Adds the single user rotation of the master password to this instance.
*
* @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation
* before Secrets Manager triggers the next automatic rotation.
*/
public addRotationSingleUser(id: string, options: SecretRotationOptions = {}): SecretRotation {
public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation {
if (!this.secret) {
throw new Error('Cannot add single user rotation for an instance without secret.');
}
return new SecretRotation(this, id, {

const id = 'RotationSingleUser';
const existing = this.node.tryFindChild(id);
if (existing) {
throw new Error('A single user rotation was already added to this instance.');
}

return new secretsmanager.SecretRotation(this, id, {
secret: this.secret,
application: this.secretRotationApplication,
automaticallyAfter,
application: this.singleUserRotationApplication,
vpc: this.vpc,
vpcSubnets: this.vpcPlacement,
target: this,
});
}

/**
* Adds the multi user rotation to this instance.
*/
public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation {
if (!this.secret) {
throw new Error('Cannot add multi user rotation for an instance without secret.');
}
return new secretsmanager.SecretRotation(this, id, {
secret: options.secret,
masterSecret: this.secret,
automaticallyAfter: options.automaticallyAfter,
application: this.multiUserRotationApplication,
vpc: this.vpc,
vpcSubnets: this.vpcPlacement,
target: this,
...options
});
}
}
Expand Down Expand Up @@ -750,7 +781,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas
constructor(scope: Construct, id: string, props: DatabaseInstanceProps) {
super(scope, id, props);

let secret;
let secret: DatabaseSecret | undefined;
if (!props.masterUserPassword) {
secret = new DatabaseSecret(this, 'Secret', {
username: props.masterUsername,
Expand Down Expand Up @@ -782,9 +813,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas
});

if (secret) {
this.secret = secret.addTargetAttachment('AttachedSecret', {
target: this
});
this.secret = secret.attach(this);
}

this.setLogRetention();
Expand Down Expand Up @@ -882,9 +911,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
});

if (secret) {
this.secret = secret.addTargetAttachment('AttachedSecret', {
target: this
});
this.secret = secret.attach(this);
}

this.setLogRetention();
Expand Down
Loading

0 comments on commit b700b77

Please sign in to comment.