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

Support SecretValue for DatabaseCluster masterUser.username #7927

Closed
2 tasks
asnaseer-resilient opened this issue May 12, 2020 · 4 comments · Fixed by #10458
Closed
2 tasks

Support SecretValue for DatabaseCluster masterUser.username #7927

asnaseer-resilient opened this issue May 12, 2020 · 4 comments · Fixed by #10458
Assignees
Labels
@aws-cdk/aws-rds Related to Amazon Relational Database effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p1
Milestone

Comments

@asnaseer-resilient
Copy link

asnaseer-resilient commented May 12, 2020

When we used Cloudformation directly, we used to set the master username and password for our DB cluster like this:

...,
"MasterUsername":{"Fn::Sub":"{{resolve:secretsmanager:DB_CREDS:SecretString:username}}"},
"MasterUserPassword":{"Fn::Sub":"{{resolve:secretsmanager:DB_CREDS:SecretString:password}}"},
...

With the CDK, it seems like the credentials are defined as:

    const secret = new Secret(this, 'MyDBSecret', {
      secretName: 'DB_PASSWORD',
      generateSecretString: {
        excludePunctuation: true
      }
    });

    const dbCluster = new DatabaseCluster(this, 'MyDbCluster', {
      ...,
      masterUser: {
        username: 'myUserName',
        password: secret.secretValue
      },
      ...
    });

So only the password can be secure.

It would be nice to be able to set both the username and password based on a JSON secret value of the form:

{
   "username": "myUserName",
   "password": "dontTellAnyone!"
}

I am currently using this workaround to achieve what I want:

    const username = SecretValue.secretsManager(DB_SECRET, { jsonField: 'username' }).toString();
    const password = SecretValue.secretsManager(DB_SECRET, { jsonField: 'password' });

    const dbCluster = new DatabaseCluster(this, 'MyDbCluster', {
      ...,
      masterUser: {
        username
        password
      },
      ...
    });

Use Case

For improved security.

Proposed Solution

In the Login data type, define the username field as string | SecretValue. This might make it more obvious to others that they can also secure the username but, at the same time, keep this backwards compatible.

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

This is a 🚀 Feature Request

@asnaseer-resilient asnaseer-resilient added feature-request A feature should be added or improved. needs-triage This issue or PR still needs to be triaged. labels May 12, 2020
@SomayaB SomayaB added the @aws-cdk/aws-secretsmanager Related to AWS Secrets Manager label May 12, 2020
@skinny85 skinny85 added @aws-cdk/aws-rds Related to Amazon Relational Database and removed @aws-cdk/aws-secretsmanager Related to AWS Secrets Manager labels Jun 29, 2020
@skinny85 skinny85 assigned nija-at and unassigned skinny85 Jun 29, 2020
@nija-at nija-at assigned skinny85 and unassigned nija-at Jul 14, 2020
@skinny85
Copy link
Contributor

skinny85 commented Sep 9, 2020

Hey @asnaseer-resilient ,

thanks for opening the issue, but I'm not sure what you're asking for here. The code you show for how you're achieving that today is exactly how I would do it. What's your ask here?

Thanks,
Adam

@skinny85 skinny85 added the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Sep 9, 2020
@asnaseer-resilient
Copy link
Author

The current code uses the secrets differently for the username vs the password, e.g. in here:

    const username = SecretValue.secretsManager(DB_SECRET, { jsonField: 'username' }).toString();
    const password = SecretValue.secretsManager(DB_SECRET, { jsonField: 'password' });

    const dbCluster = new DatabaseCluster(this, 'MyDbCluster', {
      ...,
      masterUser: {
        username
        password
      },
      ...
    });

notice that the username needs a .toString() (i.e. as a String data type in Typescript) before it is passed into the masterUser structure whereas the password value can be passed in as-is (i.e. as SecretValue data type in Typescript).

It would be nice to have them both consistent - something like this:

    const username = SecretValue.secretsManager(DB_SECRET, { jsonField: 'username' });
    const password = SecretValue.secretsManager(DB_SECRET, { jsonField: 'password' });

    const dbCluster = new DatabaseCluster(this, 'MyDbCluster', {
      ...,
      masterUser: {
        username
        password
      },
      ...
    });

@SomayaB SomayaB removed needs-triage This issue or PR still needs to be triaged. response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. labels Sep 9, 2020
@skinny85 skinny85 added effort/small Small work item – less than a day of effort p1 labels Sep 11, 2020
@skinny85 skinny85 added this to the [GA] RDS milestone Sep 11, 2020
@ericzbeard ericzbeard assigned njlynch and unassigned skinny85 Sep 16, 2020
@njlynch
Copy link
Contributor

njlynch commented Sep 18, 2020

So here's a quick overview of the options as they exist today:

DatabaseInstanceProps {
  masterUsername: string;
  masterUserPassword?: secretsmanager.SecretValue;
  masterUserPasswordEncryptionKey?: kms.Key;
}

DatabaseInstanceFromSnapshotProps {
  masterUsername: string;  // Can only be specified if generateMasterUserPassword is true
  masterUserPassword?: secretsmanager.SecretValue; // Can only be specified if generateMasterUserPassword is false
  masterUserPasswordEncryptionKey?: kms.Key; // Only applies if generateMasterUserPassword is true
  generateMasterUserPassword?: boolean;
}

DatabaseClusterProps {
  masterUser: Login;
}

Login {
  username: string;
  password?: secretsmanager.SecretValue;
  encryptionKey?: kms.Key;
}

DatabaseClusterFromSnapshotProps {
  // No login-related props
}

Certainly, the inconsistency between the individual properties (on Instances) and the Login interface (for Clusters) is something that could be addressed. As far as this feature request itself, I see four options:

Option 1:

Introduce a new optional property to Login (usernameSecretValue?) and make username optional as well. Introduce logic in the cluster -- and if we port Login over to instances, there as well -- to check that only one value or the other is specified.

Login {
  username?: string;
  usernameSecretValue?: secretsmanager.SecretValue;
  password?: secretsmanager.SecretValue;
  encryptionKey?: kms.Key;
}

This places the burden on the instance & cluster classes to validate everything's ok, and doesn't really seem to add to the aesthetics/usability of the API as a whole (IMO).

Option 2:

Introduce a new optional property to Login to replace all of the above fields with a Secret that has a defined structure:

Login {
  username?: string;
  password?: secretsmanager.SecretValue;
  encryptionKey?: kms.Key;
  secret?: secretsmanager.Secret; // Must have a "username" and "password" field in the secret body.
}

The usage here would be to either specify one (or more) of the existing fields, or just the new secret field, which would be used for the username, password, and key. Once again, the constructs would need to validate the version of the Login they receive has the right mix of optional fields, which is ugly, but this is probably the nicest balance of "backwards compatible" and "enables nicer use for those with existing secrets".

Option 3:

Replace the current Login interface with a new class that would offer a few different options for login.

class Login {
  public static fromSecret(secret: Secret): Login // Assumes secret with "username" and "password" fields.
  public static fromUsername(username: string, password?: SecretValue, key?: Key): Login
  public readonly secret: Secret;
}

DatabaseInstanceProps {
  masterUser?: Login;
}

DatabaseClusterProps {
  masterUser?: Login;
}

This is perhaps the most structured/modeled, but is also a definite breaking change for every CDK RDS customer. It's not clear that the value here is worth the breaking change.

Option 4:

Do nothing; decide that this consistency for applying existing secrets isn't worth the extra (potentially confusing) props or breaking changes.

Open to feedback from the community here; I'll also be conferring internally to see what the team thinks.

@skinny85
Copy link
Contributor

I vote for option 3, and including a nice message in the Changelog explaining how to migrate. I don't think it will be very intrusive 🙂.

njlynch added a commit that referenced this issue Sep 21, 2020
See #7927 (comment) for
motivation and design.

The current way of specifying master user logins for `DatabaseInstance` and
`DatabaseCluster` is inconsistent between the two and introduces some awkward
usage when creating a login from an existing `Secret`.

This change converts the existing `Login` interface (used by the `DatabaseCluster`)
into a class with factory methods for username/password or secret-based logins.
This also then re-uses that same interface for `DatabaseInstance`.

The one exception now will be `DatabaseInstanceFromSnapshot`, which has specific
requirements that deserved its own interface (`SnapshotLogin`).

As a side effect of this approach, existing `DatabaseCluster` users -- in
Typescript at least -- will not be broken. For example, the following are
equivalent:

```ts
new rds.DatabaseCluster(this, 'Cluster1', {
  // Existing usage
  masterUser: {
    username: 'admin',
  },
  // New usage
  masterUser: Login.fromUsername('admin'),
});
```

fixes #7927

BREAKING CHANGE: `DatabaseInstanceProps` and `DatabaseInstanceFromSnapshotProps` -
`masterUsername`, `masterUserPassword` and `masterUserPasswordEncryptionKey` moved
to `masterUser` as a new `Login` class.
* **rds:** `Login` converted from interface to class with factory methods. Use `Login.fromUsername` to replace
  existing usage.
njlynch added a commit that referenced this issue Sep 21, 2020
See #7927 (comment) for
motivation and design.

The current way of specifying master user logins for `DatabaseInstance` and
`DatabaseCluster` is inconsistent between the two and introduces some awkward
usage when creating a login from an existing `Secret`.

This change converts the existing `Login` interface (used by the `DatabaseCluster`)
into a class with factory methods for username/password or secret-based logins.
This also then re-uses that same interface for `DatabaseInstance`.

The one exception now will be `DatabaseInstanceFromSnapshot`, which has specific
requirements that deserved its own interface (`SnapshotLogin`).

As a side effect of this approach, existing `DatabaseCluster` users -- in
Typescript at least -- will not be broken. For example, the following are
equivalent:

```ts
new rds.DatabaseCluster(this, 'Cluster1', {
  // Existing usage
  masterUser: {
    username: 'admin',
  },
  // New usage
  masterUser: Login.fromUsername('admin'),
});
```

fixes #7927

BREAKING CHANGE: `DatabaseInstanceProps` and `DatabaseInstanceFromSnapshotProps` -
`masterUsername`, `masterUserPassword` and `masterUserPasswordEncryptionKey` moved
to `masterUser` as a new `Login` class.
* **rds:** `Login` converted from interface to class with factory methods. Use `Login.fromUsername` to replace
  existing usage.
@mergify mergify bot closed this as completed in #10458 Sep 23, 2020
mergify bot pushed a commit that referenced this issue Sep 23, 2020
#10458)

See #7927 (comment) for motivation and design.

The current way of specifying master user logins for `DatabaseInstance` and
`DatabaseCluster` is inconsistent between the two and introduces some awkward
usage when creating a login from an existing `Secret`.

This change converts the existing `Login` interface (used by the `DatabaseCluster`)
into a class with factory methods for username/password or secret-based logins.
This also then re-uses that same interface for `DatabaseInstance`.

The one exception now will be `DatabaseInstanceFromSnapshot`, which has specific
requirements that deserved its own interface (`SnapshotLogin`).

As a side effect of this approach, existing `DatabaseCluster` users -- in
Typescript at least -- will not be broken. For example, the following are
equivalent:

```ts
new rds.DatabaseCluster(this, 'Cluster1', {
  // Existing usage
  masterUser: {
    username: 'admin',
  },
  // New usage
  masterUser: Login.fromUsername('admin'),
});
```

Lastly, this change makes the whole `masterUser` prop optional, as there's no good reason why we can't default a username.

fixes #7927

BREAKING CHANGE: `DatabaseInstanceProps` and `DatabaseInstanceFromSnapshotProps` -
`masterUsername`, `masterUserPassword` and `masterUserPasswordEncryptionKey` moved
to `credentials` as a new `Credentials` class.
* **rds:** `Login` renamed to `Credentials`. Use `Credentials.fromUsername` to replace existing usage.
* **rds:** `DatabaseClusterProps` `masterUser` renamed to `credentials`.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-rds Related to Amazon Relational Database effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p1
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants