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

fix(rds): customizing secret results in unusable password and lost attachment #11237

Merged
merged 27 commits into from
Nov 6, 2020

Conversation

jogold
Copy link
Contributor

@jogold jogold commented Oct 31, 2020

When the excludeCharacters prop is updated the secret is correctly
regenerated but the database is not updated because the
MasterUserPassword prop does not change. Moreover the connection
fields created by the attachment are lost because the
AWS::SecretsManager::SecretAttachment doesn't "rerun" (it references
the same secret).

Introduce fromGeneratedSecret() and fromPassword() static methods
in Credentials. When used the MasterUsername prop of the database
references the username as a string instead of a dynamic reference to
the username field of the secret. Moreover the logical id of the secret
is a hash of its customization options. This way the secret gets replaced
when it's customized, the database gets updated and the attachement reruns.
This without updating the MasterUsername prop, avoiding a replacement
of the database.

For instances that were created from a snapshot the MasterUsername prop
is not specified so there's no replacement risk. Add a new static method
fromGeneratedSecret() in SnapshotCredentials to replace the secret
when its customization options change (also hash in logical id).

Deprecate Credentials.fromUsername() but keep it as default to avoid
a breaking change that would replace existing databases that were not
created from a snapshot.

Deprecate SnapshotCredentials.fromGeneratedPassword() which doesn't
replace the secret when customization options are changed.

Closes #11040


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

…tachment

When the `excludeCharacters` prop is updated the secret is correctly
regenerated but the database is not updated because the
`MasterUserPassword` prop does not change. Moreover the connection
fields created by the attachment are lost because the secret is
regenerated but the `AWS::SecretsManager::SecretAttachment` doesn't
"rerun".

Introduce a new `fromFixedUsername()` static method in `Credentials`.
When used the `MasterUsername` prop of the database references the
username as a string instead of a dynamic reference to the username
field of the secret. Moreover the logical id of the secret is a hash of
its customization options. This way the secret gets replaced when it's
customized, the database gets updated and the attachement reruns. This
without updating the `MasterUsername` prop, avoiding a replacement of
the database.

Closes aws#11040
@gitpod-io
Copy link

gitpod-io bot commented Oct 31, 2020

Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your efforts @jogold ! Some initial comments I have.

packages/@aws-cdk/aws-rds/lib/database-secret.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/lib/instance.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/lib/props.ts Outdated Show resolved Hide resolved
* Creates Credentials for the given username, and optional password and key.
* If no password is provided, one will be generated and stored in SecretsManager.
*/
public static fromFixedUsername(username: string, options: CredentialsFromUsernameOptions = {}): Credentials {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have a mechanism for making the username optional, by defaulting to either the engine-specific default, or admin if that wasn't provided... Should we make username here optional too? Maybe through something like

export interface CredentialsFromFixedUsernameOptions extends CredentialsFromUsernameOptions {
  readonly username?: string;
}

export abstract class Credentials {
  public static fromFixedUsername(options: CredentialsFromFixedUsernameOptions = {}): Credentials {
    // ...
  }
}

?

Copy link
Contributor Author

@jogold jogold Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is currently handled in the instance/cluster code when credentials is not specified. To avoid a breaking change it still falls back to fromUsername(). Do you want to move the default handling in here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it makes sense for customers to do something like this:

new rds.DatabaseCluster(this, 'Cluster', {
  // ...
  credentials: rds.Credentials.fromFixedUsername({ excludeCharacters: '!&*^#@()' }),
});

"I don't care about the username - just use the default, but I want to change something about the other properties"

Does something like this make sense?

Copy link
Contributor Author

@jogold jogold Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Credentials has a username property that cannot be undefined, I think this change means that we need to make Credentials "engine aware" to find the right default? What do you suggest?

packages/@aws-cdk/aws-rds/lib/props.ts Show resolved Hide resolved
masterUsername: credentials.username,
masterUsername: props.credentials?.usernameAsString
? props.credentials.username
: credentials.username,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code reads kind of weird, I have to say. I'm wondering whether we're making Credentials too low-level. It seems like the Instance class shouldn't have to figure out all of these details... it should be able to say masterUsername: credentials.username, and the correct value should be returned from the Credentials class itself...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, will see how to improve this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I have here is that the code currently calls fromSecret(new DatabaseSecret(...)) when the user specifies fromUsername() without password.

fromSecret() expects a secretsmanager.Secret so it cannot know when to render the username as a string. To do that I'll have to add an additional username argument to fromSecret() and specify it based on the new usernameAsString. This means that it's still the instance responsibility? Any suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about creating a new, module-private factory for Credentials that we can use here? Like _fromSecretUsernameRendered(secret: ISecret, usernameAsString: boolean): Credentials?

Copy link
Contributor Author

@jogold jogold Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll (also) need to pass the username, right?

Because you cannot extract a field out of a ISecret without creating a dynamic reference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure 🙂

Then maybe it's enough to have a username?: string as the second argument, and if it's undefined, you use the reference to the Secret? I'm sure you'll figure out the details 🙂.

Copy link
Contributor Author

@jogold jogold Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chosen to add a optional username to fromSecret() which allows users using fromSecret() to also have the behavior of the username referenced as a string.

The same kind of logic is now a few lines above.

Comment on lines 505 to 510
masterUsername: credentials.username,
masterUsername: props.credentials?.usernameAsString
? props.credentials.username
: credentials.username,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here as in Instance - there's something off about the code here, it's too low-level.

@@ -534,7 +534,7 @@
]
}
},
"InstanceSecret478E0A47": {
"653e16d6b669bc291aa4827a4721a98c": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add either id, or even uniqueId, to the generated hash - right now, these are too unreadable as just random hashes.

@@ -301,7 +301,7 @@ export = {
expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', {
MasterUsername: ABSENT,
MasterUserPassword: {
'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'InstanceSecret478E0A47' }, ':SecretString:password::}}']],
'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: '29ae4bfb9ee908ebe1611900a1e2469e' }, ':SecretString:password::}}']],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change needed? Nothing changed in the body of this test, so this is very worrying that this needs updating... This would mean a database replacement for a customer when they upgrade...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a DatabaseFromSnaphot() so its secret logical id is now a hash. As the test shows MasterUsername is expected to be ABSENT so there won't be any replacement.

packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts Outdated Show resolved Hide resolved
Co-authored-by: Adam Ruka <adamruka85@gmail.com>
@mergify mergify bot dismissed skinny85’s stale review November 2, 2020 22:45

Pull request has been modified.

@jogold
Copy link
Contributor Author

jogold commented Nov 3, 2020

@skinny85 I finally decided to introduce two new static methods to replace fromUsername(): fromGeneratedPassword() and fromPassword(). The naming is better than fromFixedUsername() which has no real meaning for people starting with aws-rds (it's more a reference to a bug fix than what it really does).

Since making username optional in Credentials is a breaking change those methods still require a username.

I also moved common code to a new util.

Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. We're almost there. A couple of minor notes.

packages/@aws-cdk/aws-rds/lib/props.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/README.md Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/README.md Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/README.md Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/lib/cluster.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/lib/database-secret.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-rds/lib/props.ts Outdated Show resolved Hide resolved
const cfnSecret1 = db1Secret.node.defaultChild as cdk.CfnResource;
const db2Secret = db2.node.tryFindChild('Secret') as rds.DatabaseSecret;
const cfnSecret2 = db2Secret.node.defaultChild as cdk.CfnResource;
test.notEqual(cfnSecret1.logicalId, cfnSecret2.logicalId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logicalIds are tokens, so you need to resolve them first before you do the comparison (try it be attempting to make this test fail by changing the hashing algorithm, it shouldn't work - this assertion should always pass).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course! moved these kind of assertions to test.database-secret.ts where it actually belongs.

@jogold jogold requested a review from skinny85 November 3, 2020 20:58
Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're almost there! I have 2 minor comments/questions.

// Use here the options that influence the password generation.
// If at some point we add other password customization options
// they sould be added here below (e.g. `passwordLength`).
excludeCharacters: props.excludeCharacters,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So excludeCharacters is actually optional. Which means this object will have undefined there as the default. Is that a mistake? Should the default value be filled here? (See line 66 above)

// they sould be added here below (e.g. `passwordLength`).
excludeCharacters: props.excludeCharacters,
}));
const logicalId = `${this.node.id.replace(/[^A-Za-z0-9]/g, '')}${hash.digest('hex')}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like id is too short here. You'll wind up with Secret<some-hash> everywhere, which is too ambiguous. Can you use uniqueId here instead? (No need for the replace part then).

*
* @default false
*/
readonly overrideLogicalId?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether a name like replaceOnPasswordChanges might be better here?

@jogold
Copy link
Contributor Author

jogold commented Nov 4, 2020

@skinny85 I think we're good here.

I added a BREAKING CHANGE to inform that the master password of instances created from snapshot will be updated. Can you check it?

@njlynch you might want to have a look at this PR since you introduced the Credentials class. Thanks!

@skinny85
Copy link
Contributor

skinny85 commented Nov 4, 2020

I added a BREAKING CHANGE to inform that the master password of instances created from snapshot will be updated. Can you check it?

This module is now stable. We can't have breaking changes to it. Please revert the changes to DatabaseInstanceFromSnapshot.

@jogold
Copy link
Contributor Author

jogold commented Nov 4, 2020

I added a BREAKING CHANGE to inform that the master password of instances created from snapshot will be updated. Can you check it?

This module is now stable. We can't have breaking changes to it. Please revert the changes to DatabaseInstanceFromSnapshot.

Got it. But do you really consider this as a breaking change? If the secret is consumed programmatically or if rotation is in place it will have no impact... your call. The password will change but the new one is available in Secrets Manager, you never loose access.

packages/@aws-cdk/aws-rds/README.md Outdated Show resolved Hide resolved
*
* @default false
*/
readonly replaceOnPasswordChanges?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be more appropriately named replaceOnPasswordCriteriaChanges? or something like that? I'm afraid the name and documentation as-is may mislead people to think the secret may be replaced when rotation is configured.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with this name, it's indeed better. @skinny85 do you validate this name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 🙂

@jogold
Copy link
Contributor Author

jogold commented Nov 4, 2020

I added a BREAKING CHANGE to inform that the master password of instances created from snapshot will be updated. Can you check it?

This module is now stable. We can't have breaking changes to it. Please revert the changes to DatabaseInstanceFromSnapshot.

Got it. But do you really consider this as a breaking change? If the secret is consumed programmatically or if rotation is in place it will have no impact... your call. The password will change but the new one is available in Secrets Manager, you never loose access.

If you want to avoid this we can expose the replaceOnPasswordChanges prop in SnapshotCredentials.fromGeneratedPassword() so that users can opt-in for the new behavior.

@skinny85
Copy link
Contributor

skinny85 commented Nov 4, 2020

I added a BREAKING CHANGE to inform that the master password of instances created from snapshot will be updated. Can you check it?

This module is now stable. We can't have breaking changes to it. Please revert the changes to DatabaseInstanceFromSnapshot.

Got it. But do you really consider this as a breaking change? If the secret is consumed programmatically or if rotation is in place it will have no impact... your call. The password will change but the new one is available in Secrets Manager, you never loose access.

If you want to avoid this we can expose the replaceOnPasswordChanges prop in SnapshotCredentials.fromGeneratedPassword() so that users can opt-in for the new behavior.

I don't love that. How about new static factory methods in SnapshotCredentials, similar to the new static factory methods in Credentials?

@jogold
Copy link
Contributor Author

jogold commented Nov 5, 2020

I don't love that. How about new static factory methods in SnapshotCredentials, similar to the new static factory methods in Credentials?

Any suggestion for the name? I would expect fromGeneratedPassword() in Credentials and SnapshotCredentials to behave the same way but it will not be the case if we add a new static method in SnapshotCredentials.

How about fromGeneratedSecret() both in Credentials and SnapshotCredentials then?

@skinny85
Copy link
Contributor

skinny85 commented Nov 5, 2020

I don't love that. How about new static factory methods in SnapshotCredentials, similar to the new static factory methods in Credentials?

Any suggestion for the name? I would expect fromGeneratedPassword() in Credentials and SnapshotCredentials to behave the same way but it will not be the case if we add a new static method in SnapshotCredentials.

How about fromGeneratedSecret() both in Credentials and SnapshotCredentials then?

That's a good point about consistency between Credentials and SnapshotCredentials.

I like fromGeneratedSecret() well enough.

@jogold jogold requested a review from skinny85 November 5, 2020 20:59
skinny85
skinny85 previously approved these changes Nov 5, 2020
Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for taking care of this @jogold !

@mergify mergify bot dismissed skinny85’s stale review November 6, 2020 17:23

Pull request has been modified.

@jogold
Copy link
Contributor Author

jogold commented Nov 6, 2020

@skinny85 can you reapprove here? Had to merge from master because the build was stuck...

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 87552f5
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@mergify
Copy link
Contributor

mergify bot commented Nov 6, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify bot merged commit a4567f5 into aws:master Nov 6, 2020
@jogold jogold deleted the rds-fixed-username branch November 6, 2020 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RDS] Issue with credentials & Secret attachment
4 participants