Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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
20 changes: 18 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,20 @@ export interface DatabaseSecretProps {
*/
readonly replaceOnPasswordCriteriaChanges?: boolean;

/**
* Whether to generate a URL parser-compatible password by excluding characters that can cause issues in URL parsers.
*
* When enabled, the generated password will exclude the caret (^) character in addition to the default
* exclusion set. This specifically addresses compatibility issues with URL parsers like Go's net/url
* that fail when parsing URLs containing caret characters in the userinfo section.
*
* Note: The default exclusion set already excludes most URL-problematic characters (%, #, ?, &, @, /, etc.).
* This option adds the caret (^) character which is specifically problematic for certain URL parsers.
*
* @default false
*/
readonly urlSafePassword?: boolean;

/**
* A list of regions where to replicate this secret.
*
Expand All @@ -84,7 +98,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 +126,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 + '^';
63 changes: 63 additions & 0 deletions packages/aws-cdk-lib/aws-rds/lib/props.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IParameterGroup } from './parameter-group';
import { DEFAULT_PASSWORD_EXCLUDE_CHARS, URL_SAFE_PASSWORD_EXCLUDE_CHARS } from './private/util';
import * as ec2 from '../../aws-ec2';
import * as kms from '../../aws-kms';
import * as secretsmanager from '../../aws-secretsmanager';
Expand Down Expand Up @@ -175,6 +176,20 @@ export interface CredentialsBaseOptions {
* @default - Secret is not replicated
*/
readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL parser-compatible password by excluding characters that can cause issues in URL parsers.
*
* When enabled, the generated password will exclude the caret (^) character in addition to the default
* exclusion set. This specifically addresses compatibility issues with URL parsers like Go's net/url
* that fail when parsing URLs containing caret characters in the userinfo section.
*
* Note: The default exclusion set already excludes most URL-problematic characters (%, #, ?, &, @, /, etc.).
* This option adds the caret (^) character which is specifically problematic for certain URL parsers.
*
* @default false
*/
readonly urlSafePassword?: boolean;
}

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

/**
* Whether to generate a URL parser-compatible 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 in addition to the default
* exclusion set. This specifically addresses compatibility issues with URL parsers like Go's net/url
* that fail when parsing URLs containing caret characters in the userinfo section.
*
* Note: The default exclusion set already excludes most URL-problematic characters (%, #, ?, &, @, /, etc.).
* This option adds the caret (^) character which is specifically problematic for certain URL parsers.
*
* @default false
*/
public abstract readonly urlSafePassword?: boolean;
}

/**
Expand All @@ -341,6 +371,20 @@ export interface SnapshotCredentialsFromGeneratedPasswordOptions {
* @default - Secret is not replicated
*/
readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL parser-compatible password by excluding characters that can cause issues in URL parsers.
*
* When enabled, the generated password will exclude the caret (^) character in addition to the default
* exclusion set. This specifically addresses compatibility issues with URL parsers like Go's net/url
* that fail when parsing URLs containing caret characters in the userinfo section.
*
* Note: The default exclusion set already excludes most URL-problematic characters (%, #, ?, &, @, /, etc.).
* This option adds the caret (^) character which is specifically problematic for certain URL parsers.
*
* @default false
*/
readonly urlSafePassword?: boolean;
}

/**
Expand All @@ -354,8 +398,12 @@ export abstract class SnapshotCredentials {
* Note - The username must match the existing master username of the snapshot.
*/
public static fromGeneratedSecret(username: string, options: SnapshotCredentialsFromGeneratedPasswordOptions = {}): SnapshotCredentials {
const excludeCharacters = options.excludeCharacters ??
(options.urlSafePassword ? URL_SAFE_PASSWORD_EXCLUDE_CHARS : DEFAULT_PASSWORD_EXCLUDE_CHARS);

return {
...options,
excludeCharacters,
generatePassword: true,
replaceOnPasswordCriteriaChanges: true,
username,
Expand Down Expand Up @@ -464,6 +512,21 @@ export abstract class SnapshotCredentials {
* @default - Secret is not replicated
*/
public abstract readonly replicaRegions?: secretsmanager.ReplicaRegion[];

/**
* Whether to generate a URL parser-compatible 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 in addition to the default
* exclusion set. This specifically addresses compatibility issues with URL parsers like Go's net/url
* that fail when parsing URLs containing caret characters in the userinfo section.
*
* Note: The default exclusion set already excludes most URL-problematic characters (%, #, ?, &, @, /, etc.).
* This option adds the caret (^) character which is specifically problematic for certain URL parsers.
*
* @default false
*/
public abstract readonly urlSafePassword?: boolean;
}

/**
Expand Down
25 changes: 25 additions & 0 deletions packages/aws-cdk-lib/aws-rds/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4725,6 +4725,31 @@ describe('cluster', () => {
});
});

test('fromGeneratedSecret with urlSafePassword', () => {
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

new DatabaseClusterFromSnapshot(stack, 'Database', {
engine: DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_3_07_1 }),
instanceProps: {
vpc,
},
snapshotIdentifier: 'mySnapshot',
snapshotCredentials: SnapshotCredentials.fromGeneratedSecret('admin', {
urlSafePassword: true,
}),
});

Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::Secret', {
GenerateSecretString: {
ExcludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/@"\\^',
GenerateStringKey: 'password',
PasswordLength: 30,
SecretStringTemplate: '{"username":"admin"}',
},
});
});

test('throws if generating a new password without a username', () => {
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');
Expand Down
Loading
Loading