aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.5k stars 3.85k forks source link

aws-secretsmanager: Include versionId / versionStage in secrets in Cloudformation templates to be able to force changes #23645

Open TimQuist opened 1 year ago

TimQuist commented 1 year ago

Describe the feature

Hi,

I would like to propose a change to the existing way the SecretValue is synthesised from CDK code to Cloudformation templates.

Currently when updating a secret in the Secretsmanager, it generates a versionId for that secret. This versionId can be used in Cloudformation templates dynamically. Cloudformation can detect changes to the versionId and deploy resources accordingly when resources are updated.

AWS Documentation about this subject:

When looking at one of the main ways to add secrets to your Cloudformation templates through CDK, it is said that the following needs to be done when changing secrets: If you rotate the value in the Secret, you must also change at least one property on the resource where you are using the secret, to force CloudFormation to re-read the secret.

Full snippit:

/**
     * Creates a `SecretValue` with a value which is dynamically loaded from AWS Secrets Manager.
     *
     * If you rotate the value in the Secret, you must also change at least one property
     * on the resource where you are using the secret, to force CloudFormation to re-read the secret.
     *
     * @param secretId The ID or ARN of the secret
     * @param options Options
     */
    static secretsManager(secretId: string, options?: SecretsManagerSecretOptions): SecretValue;

However this seems unnecessary. If CDK would include the versionId behind the secret reference (like Cloudformation currently already supports) the change of version would automatically be detected.

There of course might be something I am overlooking here, but I could not find much about this versionId property in the CDK spec. You are able to specify it when retrieving secrets using SecretsManagerSecretOptions but then it still is not added to the Cloudformation template.

Use Case

My main use case for this would be to make it easier to rotate secrets.

With the current method I have to:

By using the versionId I would not have to deploy a second time to reset the property back to normal.

Proposed Solution

Add the ability through a toggle feature to add the versionId to the Cloudformation template.

Current Example of Cloudformation template created through CDK currently using the secretsManager() function:

"ClientSecret": "{{resolve:secretsmanager:mysecret:SecretString:MyKey::}}"

Desired Example of Cloudformation template desired that CDK would create in this case:

"ClientSecret": "{{resolve:secretsmanager:mysecret:SecretString:MyKey::ab01234c-5d67-89ef-01gh-2ijk345l6m78}}"

Other Information

No response

Acknowledgements

CDK version used

2.50.0

Environment details (OS name and version, etc.)

MacOs 12.3

TimQuist commented 1 year ago

Escape hatch in case anyone else wants to use the versionId as well:

    // Retrieve versionId or set somewhere in config..
    const ref = new CfnDynamicReference(
      CfnDynamicReferenceService.SECRETS_MANAGER,
      `mysecret:SecretString:MyKey::${versionId}`
    );
    return SecretValue.cfnDynamicReference(ref);
pahud commented 1 year ago

Thank you for this idea. I am making this as p2 feature request and any community upvotes would be welcome and appreciated here.

gbiv commented 1 year ago

Escape hatch in case anyone else wants to use the versionId as well:

    // Retrieve versionId or set somewhere in config..
    const ref = new CfnDynamicReference(
      CfnDynamicReferenceService.SECRETS_MANAGER,
      `mysecret:SecretString:MyKey::${versionId}`
    );
    return SecretValue.cfnDynamicReference(ref);

@TimQuist , any tips on how to Retrieve versionId via cloudformation or is that only available via the console or cli?

TimQuist commented 1 year ago

@gbiv I have not found a way to do this yet using CDK. CDK would be the ideal way though. Having the current(new) versionId or a list of all the versionIds with dates in the ISecret

Maybe you could figure something out using the SDK within the CDK? But i'm not sure it will return the versionId this way since I have not tried doing this.

I'm currently doing so using the CLI

Theres multiple options that will include the versionId like get-secret-value and list-secret-version-id, which you then pass into CDK. Not really ideal but it works.

TimQuist commented 1 year ago

@gbiv After looking at this further, you could do this in CDK using a custom resource:

The following is not something I have actually run due to limited time, but something along the lines of this should work to retrieve up to 10 secret with versionIds.

It is based on: https://repost.aws/knowledge-center/cdk-sdk-calls-awssdkcall

import { Construct } from "constructs";
import { AwsCustomResource } from "aws-cdk-lib/custom-resources";
import { CfnOutput } from "aws-cdk-lib";

export function getSecretCustomResource(
  scope: Construct,
) {
  const resource = new AwsCustomResource(scope, "ListSecretVersionIds", {
    onCreate: {
      service: "SecretsManager",
      //https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/command/ListSecretsCommand/
      action: "ListSecrets",
      parameters: {
        //https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_Filter.html
        Filters: [
          {
            Key: "name",
            Values: [] //Names of the secrets you want to filter for here, max 10 items!
          },
        ],
      },
      physicalResourceId: PhysicalResourceId.of("list-secret-version-ids")
    },
    onUpdate: {
      service: "SecretsManager",
      //https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/command/ListSecretsCommand/
      action: "ListSecrets",
      parameters: {
        //https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_Filter.html
        Filters: [
          {
            Key: "name",
            Values: [] //Names of the secrets you want to filter for here, max 10 items!
          },
        ],
      },
      physicalResourceId: PhysicalResourceId.of("list-secret-version-ids")
    },
    policy: undefined //Probably want to specify an execution policy for the lambda role!
  });

  //Look here for the secret labeled AWS_CURRENT in the 'SecretVersionsToStages'
  //Probably do not want to use this for automatic rotations
  //This should return:
  // [{
  //          "ARN": "string",
  //          "CreatedDate": number,
  //          "DeletedDate": number,
  //          "Description": "string",
  //          "KmsKeyId": "string",
  //          "LastAccessedDate": number,
  //          "LastChangedDate": number,
  //          "LastRotatedDate": number,
  //          "Name": "string",
  //          "NextRotationDate": number,
  //          "OwningService": "string",
  //          "PrimaryRegion": "string",
  //          "RotationEnabled": boolean,
  //          "RotationLambdaARN": "string",
  //          "RotationRules": {
  //             "AutomaticallyAfterDays": number,
  //             "Duration": "string",
  //             "ScheduleExpression": "string"
  //          },
  //          "SecretVersionsToStages": {
  //             "string" : [ "string" ]
  //          },
  //          "Tags": [
  //             {
  //                "Key": "string",
  //                "Value": "string"
  //             }
  //          ]
  //       }]
  return new CfnOutput(scope, "secret-versions", {
    value: resource.getResponseField("SecretList"),
  }).value;
}
shoichi1023 commented 1 month ago

Any news on this issue?