Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 29 additions & 0 deletions packages/aws-cdk-lib/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,23 @@ new rds.DatabaseInstance(this, 'InstanceWithCustomizedSecret', {
});
```

For applications that embed database credentials in connection URLs (such as Go applications using `net/url` parser), you can generate URL-safe passwords that exclude characters known to cause URL parsing issues:

```ts
declare const vpc: ec2.Vpc;
const engine = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_16_3 });

new rds.DatabaseInstance(this, 'InstanceWithUrlSafePassword', {
engine,
vpc,
credentials: rds.Credentials.fromGeneratedSecret('postgres', {
urlSafePassword: true, // Excludes characters that can cause URL parsing issues (like ^)
}),
});
```

The `urlSafePassword` option extends the default character exclusion set to include characters that are problematic in URLs, particularly the caret (`^`) character which causes failures in Go's `net/url` parser. If you specify both `urlSafePassword: true` and `excludeCharacters`, the explicit `excludeCharacters` takes precedence.

### Snapshot credentials

As noted above, Databases created with `DatabaseInstanceFromSnapshot` or `ServerlessClusterFromSnapshot` will not create user and auto-generated password by default because it's not possible to change the master username for a snapshot. Instead, they will use the existing username and password from the snapshot. You can still generate a new password - to generate a secret similarly to the other constructs, pass in credentials with `fromGeneratedSecret()` or `fromGeneratedPassword()`.
Expand All @@ -816,6 +833,18 @@ new rds.DatabaseInstanceFromSnapshot(this, 'InstanceFromSnapshotWithCustomizedSe
replicaRegions: [{ region: 'eu-west-1' }, { region: 'eu-west-2' }],
}),
});

// Alternative: Generate URL-safe password for snapshot credentials
new rds.DatabaseInstanceFromSnapshot(this, 'InstanceFromSnapshotWithUrlSafePassword', {
engine,
vpc,
snapshotIdentifier: 'mySnapshot',
credentials: rds.SnapshotCredentials.fromGeneratedSecret('username', {
encryptionKey: myKey,
urlSafePassword: true, // Excludes URL-problematic characters like ^
replicaRegions: [{ region: 'eu-west-1' }, { region: 'eu-west-2' }],
}),
});
```

## Connecting
Expand Down
17 changes: 15 additions & 2 deletions packages/aws-cdk-lib/aws-rds/lib/database-secret.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Construct } from 'constructs';
import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from './private/util';
import { DEFAULT_PASSWORD_EXCLUDE_CHARS, URL_SAFE_PASSWORD_EXCLUDE_CHARS } from './private/util';
import * as kms from '../../aws-kms';
import * as secretsmanager from '../../aws-secretsmanager';
import { Aws, Names } from '../../core';
Expand Down Expand Up @@ -63,6 +63,17 @@ export interface DatabaseSecretProps {
*/
readonly replaceOnPasswordCriteriaChanges?: boolean;

/**
* Whether to generate a URL-safe password by excluding characters that can cause issues in URL parsers.
*
* When enabled, the generated password will exclude the caret (^) character and other URL-problematic
* characters in addition to the default exclusion set. This is particularly useful for applications
* that embed database credentials in connection URLs, such as Go applications using net/url parser.
*
* @default false
*/
readonly urlSafePassword?: boolean;

/**
* A list of regions where to replicate this secret.
*
Expand All @@ -84,7 +95,8 @@ export class DatabaseSecret extends secretsmanager.Secret {
public static readonly PROPERTY_INJECTION_ID: string = 'aws-cdk-lib.aws-rds.DatabaseSecret';

constructor(scope: Construct, id: string, props: DatabaseSecretProps) {
const excludeCharacters = props.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS;
const excludeCharacters = props.excludeCharacters ??
(props.urlSafePassword ? URL_SAFE_PASSWORD_EXCLUDE_CHARS : DEFAULT_PASSWORD_EXCLUDE_CHARS);

super(scope, id, {
encryptionKey: props.encryptionKey,
Expand All @@ -111,6 +123,7 @@ export class DatabaseSecret extends secretsmanager.Secret {
// If at some point we add other password customization options
// they should be added here below (e.g. `passwordLength`).
excludeCharacters,
urlSafePassword: props.urlSafePassword,
}));
const logicalId = `${Names.uniqueId(this)}${hash}`;

Expand Down
10 changes: 10 additions & 0 deletions packages/aws-cdk-lib/aws-rds/lib/private/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function renderCredentials(scope: Construct, engine: IEngine, credentials
secretName: renderedCredentials.secretName,
encryptionKey: renderedCredentials.encryptionKey,
excludeCharacters: renderedCredentials.excludeCharacters,
urlSafePassword: renderedCredentials.urlSafePassword,
// if username must be referenced as a string we can safely replace the
// secret when customization options are changed without risking a replacement
replaceOnPasswordCriteriaChanges: credentials?.usernameAsString,
Expand Down Expand Up @@ -125,6 +126,7 @@ export function renderSnapshotCredentials(scope: Construct, credentials?: Snapsh
username: renderedCredentials.username,
encryptionKey: renderedCredentials.encryptionKey,
excludeCharacters: renderedCredentials.excludeCharacters,
urlSafePassword: renderedCredentials.urlSafePassword,
replaceOnPasswordCriteriaChanges: renderedCredentials.replaceOnPasswordCriteriaChanges,
replicaRegions: renderedCredentials.replicaRegions,
}),
Expand Down Expand Up @@ -169,3 +171,11 @@ export function applyDefaultRotationOptions(options: CommonRotationUserOptions,
...options,
};
}
/**
* URL-safe password exclusion characters for database users.
* Extends the default exclusion set with characters that cause issues in URL parsers,
* particularly the caret (^) character which causes failures in Go's net/url parser.
*
* This constant is private to the RDS module.
*/
export const URL_SAFE_PASSWORD_EXCLUDE_CHARS = DEFAULT_PASSWORD_EXCLUDE_CHARS + '^';
35 changes: 35 additions & 0 deletions packages/aws-cdk-lib/aws-rds/lib/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ export interface CredentialsBaseOptions {
* @default - Secret is not replicated
*/
readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL-safe password by excluding characters that can cause issues in URL parsers.
*
* When enabled, the generated password will exclude the caret (^) character and other URL-problematic
* characters in addition to the default exclusion set. This is particularly useful for applications
* that embed database credentials in connection URLs, such as Go applications using net/url parser.
*
* @default false
*/
readonly urlSafePassword?: boolean;
}

/**
Expand Down Expand Up @@ -315,6 +326,18 @@ export abstract class Credentials {
* @default - Secret is not replicated
*/
public abstract readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL-safe password by excluding characters that can cause issues in URL parsers.
* Only used if `password` has not been set.
*
* When enabled, the generated password will exclude the caret (^) character and other URL-problematic
* characters in addition to the default exclusion set. This is particularly useful for applications
* that embed database credentials in connection URLs, such as Go applications using net/url parser.
*
* @default false
*/
public abstract readonly urlSafePassword?: boolean;
}

/**
Expand Down Expand Up @@ -464,6 +487,18 @@ export abstract class SnapshotCredentials {
* @default - Secret is not replicated
*/
public abstract readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL-safe password by excluding characters that can cause issues in URL parsers.
* Only used if `generatePassword` is true.
*
* When enabled, the generated password will exclude the caret (^) character and other URL-problematic
* characters in addition to the default exclusion set. This is particularly useful for applications
* that embed database credentials in connection URLs, such as Go applications using net/url parser.
*
* @default false
*/
public abstract readonly urlSafePassword?: boolean;
}

/**
Expand Down
136 changes: 135 additions & 1 deletion packages/aws-cdk-lib/aws-rds/test/database-secret.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Template } from '../../assertions';
import { CfnResource, Stack } from '../../core';
import { DatabaseSecret } from '../lib';
import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from '../lib/private/util';
import { DEFAULT_PASSWORD_EXCLUDE_CHARS, URL_SAFE_PASSWORD_EXCLUDE_CHARS } from '../lib/private/util';

describe('database secret', () => {
test('create a database secret', () => {
Expand Down Expand Up @@ -139,6 +139,140 @@ describe('database secret', () => {
});
expect(dbSecretlogicalId).not.toEqual(getSecretLogicalId(otherSecret2, stack));
});

test('create a database secret with URL-safe password', () => {
// GIVEN
const stack = new Stack();

// WHEN
const dbSecret = new DatabaseSecret(stack, 'Secret', {
username: 'admin-username',
urlSafePassword: true,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', {
Description: {
'Fn::Join': [
'',
[
'Generated by the CDK for stack: ',
{
Ref: 'AWS::StackName',
},
],
],
},
GenerateSecretString: {
ExcludeCharacters: URL_SAFE_PASSWORD_EXCLUDE_CHARS,
GenerateStringKey: 'password',
PasswordLength: 30,
SecretStringTemplate: '{"username":"admin-username"}',
},
});

expect(getSecretLogicalId(dbSecret, stack)).toEqual('SecretA720EF05');
});

test('URL-safe password excludes caret character', () => {
// GIVEN
const stack = new Stack();

// WHEN
new DatabaseSecret(stack, 'Secret', {
username: 'admin-username',
urlSafePassword: true,
});

// THEN
const template = Template.fromStack(stack);
const resources = template.findResources('AWS::SecretsManager::Secret');
const secretResource = Object.values(resources)[0];
const excludeCharacters = secretResource.Properties.GenerateSecretString.ExcludeCharacters;

expect(excludeCharacters).toContain('^');
expect(excludeCharacters).toEqual(URL_SAFE_PASSWORD_EXCLUDE_CHARS);
});

test('explicit excludeCharacters takes precedence over urlSafePassword', () => {
// GIVEN
const stack = new Stack();
const customExcludeChars = 'abc123';

// WHEN
new DatabaseSecret(stack, 'Secret', {
username: 'admin-username',
urlSafePassword: true,
excludeCharacters: customExcludeChars,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', {
GenerateSecretString: {
ExcludeCharacters: customExcludeChars,
},
});
});

test('urlSafePassword affects logical ID when replaceOnPasswordCriteriaChanges is true', () => {
// GIVEN
const stack = new Stack();

// WHEN
const dbSecret1 = new DatabaseSecret(stack, 'Secret1', {
username: 'admin',
replaceOnPasswordCriteriaChanges: true,
urlSafePassword: false,
});

const dbSecret2 = new DatabaseSecret(stack, 'Secret2', {
username: 'admin',
replaceOnPasswordCriteriaChanges: true,
urlSafePassword: true,
});

// THEN
const logicalId1 = getSecretLogicalId(dbSecret1, stack);
const logicalId2 = getSecretLogicalId(dbSecret2, stack);
expect(logicalId1).not.toEqual(logicalId2);
});

test('urlSafePassword with master secret', () => {
// GIVEN
const stack = new Stack();
const masterSecret = new DatabaseSecret(stack, 'MasterSecret', {
username: 'master-username',
urlSafePassword: true,
});

// WHEN
new DatabaseSecret(stack, 'UserSecret', {
username: 'user-username',
masterSecret,
urlSafePassword: true,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', {
GenerateSecretString: {
ExcludeCharacters: URL_SAFE_PASSWORD_EXCLUDE_CHARS,
GenerateStringKey: 'password',
PasswordLength: 30,
SecretStringTemplate: {
'Fn::Join': [
'',
[
'{"username":"user-username","masterarn":"',
{
Ref: 'MasterSecretA11BF785',
},
'"}',
],
],
},
},
});
});
});

function getSecretLogicalId(dbSecret: DatabaseSecret, stack: Stack): string {
Expand Down
Loading