-
Notifications
You must be signed in to change notification settings - Fork 4k
/
secret-rotation.ts
361 lines (314 loc) · 13.2 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
import { Construct } from 'constructs';
import { ISecret } from './secret';
import * as ec2 from '../../aws-ec2';
import * as lambda from '../../aws-lambda';
import * as serverless from '../../aws-sam';
import { Duration, Names, Stack, Token, CfnMapping, Aws, RemovalPolicy } from '../../core';
/**
* Options for a SecretRotationApplication
*/
export interface SecretRotationApplicationOptions {
/**
* Whether the rotation application uses the mutli user scheme
*
* @default false
*/
readonly isMultiUser?: boolean;
}
/**
* A secret rotation serverless application.
*/
export class SecretRotationApplication {
/**
* Conducts an AWS SecretsManager secret rotation for RDS MariaDB using the single user rotation scheme
*/
public static readonly MARIADB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for RDS MariaDB using the multi user rotation scheme
*/
public static readonly MARIADB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for RDS MySQL using the single user rotation scheme
*/
public static readonly MYSQL_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for RDS MySQL using the multi user rotation scheme
*/
public static readonly MYSQL_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSMySQLRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for RDS Oracle using the single user rotation scheme
*/
public static readonly ORACLE_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for RDS Oracle using the multi user rotation scheme
*/
public static readonly ORACLE_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSOracleRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for RDS PostgreSQL using the single user rotation scheme
*/
public static readonly POSTGRES_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for RDS PostgreSQL using the multi user rotation scheme
*/
public static readonly POSTGRES_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for RDS SQL Server using the single user rotation scheme
*/
public static readonly SQLSERVER_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for RDS SQL Server using the multi user rotation scheme
*/
public static readonly SQLSERVER_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for Amazon Redshift using the single user rotation scheme
*/
public static readonly REDSHIFT_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for Amazon Redshift using the multi user rotation scheme
*/
public static readonly REDSHIFT_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerRedshiftRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* Conducts an AWS SecretsManager secret rotation for MongoDB using the single user rotation scheme
*/
public static readonly MONGODB_ROTATION_SINGLE_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationSingleUser', '1.1.367');
/**
* Conducts an AWS SecretsManager secret rotation for MongoDB using the multi user rotation scheme
*/
public static readonly MONGODB_ROTATION_MULTI_USER = new SecretRotationApplication('SecretsManagerMongoDBRotationMultiUser', '1.1.367', {
isMultiUser: true,
});
/**
* The application identifier of the rotation application
*
* @deprecated only valid when deploying to the 'aws' partition. Use `applicationArnForPartition` instead.
*/
public readonly applicationId: string;
/**
* The semantic version of the rotation application
*
* @deprecated only valid when deploying to the 'aws' partition. Use `semanticVersionForPartition` instead.
*/
public readonly semanticVersion: string;
/**
* Whether the rotation application uses the mutli user scheme
*/
public readonly isMultiUser?: boolean;
/**
* The application name of the rotation application
*/
private readonly applicationName: string;
constructor(applicationId: string, semanticVersion: string, options?: SecretRotationApplicationOptions) {
// partitions are handled explicitly via applicationArnForPartition()
// eslint-disable-next-line @cdklabs/no-literal-partition
this.applicationId = `arn:aws:serverlessrepo:us-east-1:297356227824:applications/${applicationId}`;
this.semanticVersion = semanticVersion;
this.applicationName = applicationId;
this.isMultiUser = options && options.isMultiUser;
}
/**
* Returns the application ARN for the current partition.
* Can be used in combination with a `CfnMapping` to automatically select the correct ARN based on the current partition.
*/
public applicationArnForPartition(partition: string) {
if (partition === 'aws') {
return this.applicationId;
} else if (partition === 'aws-cn') {
return `arn:aws-cn:serverlessrepo:cn-north-1:193023089310:applications/${this.applicationName}`;
} else if (partition === 'aws-us-gov') {
return `arn:aws-us-gov:serverlessrepo:us-gov-west-1:023102451235:applications/${this.applicationName}`;
} else {
throw new Error(`unsupported partition: ${partition}`);
}
}
/**
* The semantic version of the app for the current partition.
* Can be used in combination with a `CfnMapping` to automatically select the correct version based on the current partition.
*/
public semanticVersionForPartition(partition: string) {
if (partition === 'aws') {
return this.semanticVersion;
} else if (partition === 'aws-cn') {
return '1.1.237';
} else if (partition === 'aws-us-gov') {
return '1.1.213';
} else {
throw new Error(`unsupported partition: ${partition}`);
}
}
}
/**
* Construction properties for a SecretRotation.
*/
export interface SecretRotationProps {
/**
* 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`
* or an `ISecret` returned by the `attach()` method of `Secret`.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html
*/
readonly secret: ISecret;
/**
* The master secret for a multi user rotation scheme
*
* @default - single user rotation scheme
*/
readonly masterSecret?: ISecret;
/**
* Specifies the number of days after the previous rotation before
* Secrets Manager triggers the next automatic rotation.
*
* @default Duration.days(30)
*/
readonly automaticallyAfter?: Duration;
/**
* 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 - the Vpc default strategy if not specified.
*/
readonly vpcSubnets?: ec2.SubnetSelection;
/**
* The target service or database
*/
readonly target: ec2.IConnectable;
/**
* The security group for the Lambda rotation function
*
* @default - a new security group is created
*/
readonly securityGroup?: ec2.ISecurityGroup;
/**
* Characters which should not appear in the generated password
*
* @default - no additional characters are explicitly excluded
*/
readonly excludeCharacters?: string;
/**
* The VPC interface endpoint to use for the Secrets Manager API
*
* If you enable private DNS hostnames for your VPC private endpoint (the default), you don't
* need to specify an endpoint. The standard Secrets Manager DNS hostname the Secrets Manager
* CLI and SDKs use by default (https://secretsmanager.<region>.amazonaws.com) automatically
* resolves to your VPC endpoint.
*
* @default https://secretsmanager.<region>.amazonaws.com
*/
readonly endpoint?: ec2.IInterfaceVpcEndpoint;
/**
* Specifies whether to rotate the secret immediately or wait until the next
* scheduled rotation window.
*
* @default true
*/
readonly rotateImmediatelyOnUpdate?: boolean;
}
/**
* Secret rotation for a service or database
*/
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.');
}
if (props.application.isMultiUser && !props.masterSecret) {
throw new Error('The `masterSecret` must be specified for application using the multi user scheme.');
}
// Max length of 64 chars, get the last 64 chars
const uniqueId = Names.uniqueId(this);
const rotationFunctionName = uniqueId.substring(Math.max(uniqueId.length - 64, 0), uniqueId.length);
const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
});
props.target.connections.allowDefaultPortFrom(securityGroup);
const parameters: { [key: string]: string } = {
endpoint: `https://${props.endpoint ? `${props.endpoint.vpcEndpointId}.` : ''}secretsmanager.${Stack.of(this).region}.${Stack.of(this).urlSuffix}`,
functionName: rotationFunctionName,
vpcSubnetIds: props.vpc.selectSubnets(props.vpcSubnets).subnetIds.join(','),
vpcSecurityGroupIds: securityGroup.securityGroupId,
};
if (props.excludeCharacters !== undefined) {
parameters.excludeCharacters = props.excludeCharacters;
}
if (props.secret.encryptionKey) {
parameters.kmsKeyArn = props.secret.encryptionKey.keyArn;
}
if (props.masterSecret) {
parameters.masterSecretArn = props.masterSecret.secretArn;
if (props.masterSecret.encryptionKey) {
parameters.masterSecretKmsKeyArn = props.masterSecret.encryptionKey.keyArn;
}
}
const sarMapping = new CfnMapping(this, 'SARMapping', {
mapping: {
'aws': {
applicationId: props.application.applicationArnForPartition('aws'),
semanticVersion: props.application.semanticVersionForPartition('aws'),
},
'aws-cn': {
applicationId: props.application.applicationArnForPartition('aws-cn'),
semanticVersion: props.application.semanticVersionForPartition('aws-cn'),
},
'aws-us-gov': {
applicationId: props.application.applicationArnForPartition('aws-us-gov'),
semanticVersion: props.application.semanticVersionForPartition('aws-us-gov'),
},
},
});
const application = new serverless.CfnApplication(this, 'Resource', {
location: {
applicationId: sarMapping.findInMap(Aws.PARTITION, 'applicationId'),
semanticVersion: sarMapping.findInMap(Aws.PARTITION, 'semanticVersion'),
},
parameters,
});
application.applyRemovalPolicy(RemovalPolicy.DESTROY);
// This creates a CF a dependency between the rotation schedule and the
// serverless application. This is needed because it's the application
// that creates the Lambda permission to invoke the function.
// See https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html
const rotationLambda = lambda.Function.fromFunctionArn(this, 'RotationLambda', Token.asString(application.getAtt('Outputs.RotationLambdaARN')));
props.secret.addRotationSchedule('RotationSchedule', {
rotationLambda,
automaticallyAfter: props.automaticallyAfter,
rotateImmediatelyOnUpdate: props.rotateImmediatelyOnUpdate,
});
// Prevent master secret deletion when rotation is in place
if (props.masterSecret) {
props.masterSecret.denyAccountRootDelete();
}
}
}