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(secretsmanager): exclude characters for hosted rotation #20768

Merged
merged 5 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotatio
dbConnections.allowDefaultPortFrom(myHostedRotation);
```

Use the `excludeCharacters` option to customize the characters excluded from
the generated password when it is rotated. By default, the rotation excludes
the same characters as the ones excluded for the secret. If none are defined
then the following set is used: ``% +~`#$&*()|[]{}:;<>?!'/@"\``.


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

## Rotating database credentials
Expand Down
23 changes: 22 additions & 1 deletion packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import { Duration, Resource, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { ISecret } from './secret';
import { ISecret, Secret } from './secret';
import { CfnRotationSchedule } from './secretsmanager.generated';

/**
* The default set of characters we exclude from generated passwords for database users.
* It's a combination of characters that have a tendency to cause problems in shell scripts,
* some engine-specific characters (for example, Oracle doesn't like '@' in its passwords),
* and some that trip up other services, like DMS.
*/
const DEFAULT_PASSWORD_EXCLUDE_CHARS = " %+~`#$&*()|[]{}:;<>?!'/@\"\\";

/**
* Options to add a rotation schedule to a secret.
*/
Expand Down Expand Up @@ -162,6 +170,14 @@ export interface SingleUserHostedRotationOptions {
* @default - the Vpc default strategy if not specified.
*/
readonly vpcSubnets?: ec2.SubnetSelection;

/**
* A string of the characters that you don't want in the password
*
* @default the same exclude characters as the ones used for the
* secret or " %+~`#$&*()|[]{}:;<>?!'/@\"\\"
*/
readonly excludeCharacters?: string,
}

/**
Expand Down Expand Up @@ -284,6 +300,10 @@ export class HostedRotation implements ec2.IConnectable {
this.masterSecret.denyAccountRootDelete();
}

const defaultExcludeCharacters = Secret.isSecret(secret)
? secret.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS
: DEFAULT_PASSWORD_EXCLUDE_CHARS;

return {
rotationType: this.type.name,
kmsKeyArn: secret.encryptionKey?.keyArn,
Expand All @@ -292,6 +312,7 @@ export class HostedRotation implements ec2.IConnectable {
rotationLambdaName: this.props.functionName,
vpcSecurityGroupIds: this._connections?.securityGroups?.map(s => s.securityGroupId).join(','),
vpcSubnetIds: this.props.vpc?.selectSubnets(this.props.vpcSubnets).subnetIds.join(','),
excludeCharacters: this.props.excludeCharacters ?? defaultExcludeCharacters,
};
}

Expand Down
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ResourcePolicy } from './policy';
import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule';
import * as secretsmanager from './secretsmanager.generated';

const SECRET_SYMBOL = Symbol.for('@aws-cdk/secretsmanager.Secret');

/**
* A secret in AWS Secrets Manager.
*/
Expand Down Expand Up @@ -446,6 +448,12 @@ abstract class SecretBase extends Resource implements ISecret {
* Creates a new secret in AWS SecretsManager.
*/
export class Secret extends SecretBase {
/**
* Return whether the given object is a Secret.
*/
public static isSecret(x: any): x is Secret {
return x !== null && typeof(x) === 'object' && SECRET_SYMBOL in x;
}

/** @deprecated use `fromSecretCompleteArn` or `fromSecretPartialArn` */
public static fromSecretArn(scope: Construct, id: string, secretArn: string): ISecret {
Expand Down Expand Up @@ -553,6 +561,12 @@ export class Secret extends SecretBase {
public readonly secretArn: string;
public readonly secretName: string;

/**
* The string of the characters that are excluded in this secret
* when it is generated.
*/
public readonly excludeCharacters?: string;

private replicaRegions: secretsmanager.CfnSecret.ReplicaRegionProperty[] = [];

protected readonly autoCreatePolicy = true;
Expand Down Expand Up @@ -609,6 +623,8 @@ export class Secret extends SecretBase {
for (const replica of props.replicaRegions ?? []) {
this.addReplicaRegion(replica.region, replica.encryptionKey);
}

this.excludeCharacters = props.generateSecretString?.excludeCharacters;
}

/**
Expand Down Expand Up @@ -925,3 +941,12 @@ function parseSecretNameForOwnedSecret(construct: Construct, secretArn: string,
function arnIsComplete(secretArn: string): boolean {
return Token.isUnresolved(secretArn) || /-[a-z0-9]{6}$/i.test(secretArn);
}

/**
* Mark all instances of 'Secret'.
*/
Object.defineProperty(Secret.prototype, SECRET_SYMBOL, {
value: true,
enumerable: false,
writable: false,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "20.0.0",
"files": {
"80e7147ae17e29a7810c1890b8caa90a140f0089dcb2dce470bd13d88e5acc41": {
"source": {
"path": "cdk-integ-secret-hosted-rotation.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "80e7147ae17e29a7810c1890b8caa90a140f0089dcb2dce470bd13d88e5acc41.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"Transform": "AWS::SecretsManager-2020-07-23",
"Transform": [
"AWS::SecretsManager-2020-07-23"
],
"Resources": {
"SecretA720EF05": {
"Type": "AWS::SecretsManager::Secret",
Expand All @@ -16,6 +18,7 @@
"Ref": "SecretA720EF05"
},
"HostedRotationLambda": {
"ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\",
"RotationType": "MySQLSingleUser"
},
"RotationRules": {
Expand Down Expand Up @@ -58,6 +61,67 @@
"Ref": "SecretA720EF05"
}
}
},
"CustomSecret5DC95D87": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"GenerateSecretString": {
"ExcludeCharacters": "&@/"
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"CustomSecretScheduleDD99F351": {
"Type": "AWS::SecretsManager::RotationSchedule",
"Properties": {
"SecretId": {
"Ref": "CustomSecret5DC95D87"
},
"HostedRotationLambda": {
"ExcludeCharacters": "&@/",
"RotationType": "MySQLSingleUser"
},
"RotationRules": {
"AutomaticallyAfterDays": 30
}
}
},
"CustomSecretPolicy8E81D2EB": {
"Type": "AWS::SecretsManager::ResourcePolicy",
"Properties": {
"ResourcePolicy": {
"Statement": [
{
"Action": "secretsmanager:DeleteSecret",
"Effect": "Deny",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
},
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"SecretId": {
"Ref": "CustomSecret5DC95D87"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"17.0.0"}
{"version":"20.0.0"}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "18.0.0",
"version": "20.0.0",
"testCases": {
"aws-secretsmanager/test/integ.hosted-rotation": {
"integ.hosted-rotation": {
"stacks": [
"cdk-integ-secret-hosted-rotation"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "17.0.0",
"version": "20.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand Down Expand Up @@ -32,6 +32,24 @@
"type": "aws:cdk:logicalId",
"data": "SecretPolicy06C9821C"
}
],
"/cdk-integ-secret-hosted-rotation/CustomSecret/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomSecret5DC95D87"
}
],
"/cdk-integ-secret-hosted-rotation/CustomSecret/Schedule/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomSecretScheduleDD99F351"
}
],
"/cdk-integ-secret-hosted-rotation/CustomSecret/Policy/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomSecretPolicy8E81D2EB"
}
]
},
"displayName": "cdk-integ-secret-hosted-rotation"
Expand Down
Loading