/
secret-rotation.ts
146 lines (123 loc) · 5.73 KB
/
secret-rotation.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import ec2 = require('@aws-cdk/aws-ec2');
import lambda = require('@aws-cdk/aws-lambda');
import serverless = require('@aws-cdk/aws-sam');
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
import { Construct, Duration, Stack } from '@aws-cdk/core';
/**
* A secret rotation serverless application.
*/
export class SecretRotationApplication {
public static readonly MARIADB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationSingleUser', '1.0.57');
public static readonly MARIADB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationMultiUser', '1.0.57');
public static readonly MYSQL_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationSingleUser', '1.0.85');
public static readonly MYSQL_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationMultiUser', '1.0.85');
public static readonly ORACLE_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationSingleUser', '1.0.56');
public static readonly ORACLE_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationMultiUser', '1.0.56');
public static readonly POSTGRES_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.0.86');
public static readonly POSTGRES_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationMultiUser ', '1.0.86');
public static readonly SQLSERVER_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationSingleUser', '1.0.57');
public static readonly SQLSERVER_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationMultiUser', '1.0.57');
public readonly applicationId: string;
public readonly semanticVersion: string;
constructor(applicationId: string, semanticVersion: string) {
this.applicationId = `arn:aws:serverlessrepo:us-east-1:297356227824:applications/${applicationId}`;
this.semanticVersion = semanticVersion;
}
}
/**
* Options to add secret rotation to a database instance or cluster.
*/
export interface SecretRotationOptions {
/**
* Specifies the number of days after the previous rotation before
* Secrets Manager triggers the next automatic rotation.
*
* @default Duration.days(30)
*/
readonly automaticallyAfter?: Duration;
}
/**
* Construction properties for a SecretRotation.
*/
export interface SecretRotationProps extends SecretRotationOptions {
/**
* The secret to rotate. It 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
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html
*/
readonly secret: secretsmanager.ISecret;
/**
* The serverless application for the rotation.
*/
readonly application: SecretRotationApplication;
/**
* The VPC where the Lambda rotation function will run.
*/
readonly vpc: ec2.IVpc;
/**
* The type of subnets in the VPC where the Lambda rotation function will run.
*
* @default - Private subnets.
*/
readonly vpcSubnets?: ec2.SubnetSelection;
/**
* The target database cluster or instance
*/
readonly target: ec2.IConnectable;
}
/**
* Secret rotation for a database instance or cluster.
*/
export class SecretRotation extends Construct {
constructor(scope: Construct, id: string, props: SecretRotationProps) {
super(scope, id);
if (!props.target.connections.defaultPort) {
throw new Error('The `target` connections must have a default port range.');
}
const rotationFunctionName = this.node.uniqueId;
const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc
});
const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets);
props.target.connections.allowDefaultPortFrom(securityGroup);
const application = new serverless.CfnApplication(this, 'Resource', {
location: props.application,
parameters: {
endpoint: `https://secretsmanager.${Stack.of(this).region}.${Stack.of(this).urlSuffix}`,
functionName: rotationFunctionName,
vpcSecurityGroupIds: securityGroup.securityGroupId,
vpcSubnetIds: subnetIds.join(',')
}
});
// Dummy import to reference this function in the rotation schedule
const rotationLambda = lambda.Function.fromFunctionArn(this, 'RotationLambda', Stack.of(this).formatArn({
service: 'lambda',
resource: 'function',
sep: ':',
resourceName: rotationFunctionName
}));
// Cannot use rotationLambda.addPermission because it's a no-op on imported
// functions.
const permission = new lambda.CfnPermission(this, 'Permission', {
action: 'lambda:InvokeFunction',
functionName: rotationFunctionName,
principal: `secretsmanager.${Stack.of(this).urlSuffix}`
});
permission.node.addDependency(application); // Add permission after application is deployed
const rotationSchedule = props.secret.addRotationSchedule('RotationSchedule', {
rotationLambda,
automaticallyAfter: props.automaticallyAfter
});
rotationSchedule.node.addDependency(permission); // Cannot rotate without permission
}
}