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(rds): more extensive secret rotation support #5281

Merged
merged 7 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it a good idea to break this API this strongly? I appreciate that the options are drastically simplified, but the following seems nicely standardized and not too onerous to me:

cluster.addRotationSingleUser('id', {
  automaticallyAfter: Duration.days(14)
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only to reflect the fact that a single rotation can only be added once to a cluster/instance so there's no point naming it...

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