Skip to content

Commit

Permalink
feat(secretsmanager): exclude characters for hosted rotation (#20768)
Browse files Browse the repository at this point in the history
By default, use the exclude characters of the secret if available or
the same default exclude characters as the ones used in `@aws-cdk/aws-rds`.

A subsequent PR should replace the secret rotation implementation in
`@aws-cdk/aws-rds` with a hosted rotation now that exclude chars are
supported.


----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold committed Jun 16, 2022
1 parent e692ad2 commit d66534a
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 10 deletions.
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

0 comments on commit d66534a

Please sign in to comment.