How can I build a CloudFormation secret out of another secret?

0

I have an image I deploy to ECS that expects an environment variable called DATABASE_URL which contains the username and password as the userinfo part of the url (e.g. postgres://myusername:mypassword@mydb.foo.us-east-1.rds.amazonaws.com:5432/mydbname). I cannot change the image.

Using DatabaseInstance.Builder.credentials(fromGeneratedSecret("myusername")), CDK creates a secret in Secrets Manager for me that has all of this information, but not as a single value:

{
  "username":"myusername",
  "password":"mypassword",
  "engine":"postgres",
  "host":"mydb.foo.us-east-1.rds.amazonaws.com",
  "port":5432,
  "dbInstanceIdentifier":"live-myproduct-db"
}

Somehow I need to synthesise that DATABASE_URL environment variable.

I don't think I can do it in the ECS Task Definition - as far as I can tell the secret can only reference a single key in a secret.

I thought I might be able to add an extra url key to the existing secret using references in cloud formation - but I can't see how. Something like:

secret.newBuilder()
  .addTemplatedKey(
    "url",
    "postgres://#{username}:#{password}@#{host}:#{port}/#{db}"
  )
  .build()

except that I just made that up...

Alternatively I could use CDK to generate a new secret in either Secrets Manager or Systems Manager - but again I want to specify it as a template so that the real secret values don't get materialised in the CloudFormation template.

Any thoughts? I'm hoping I'm just missing some way to use the API to build compound secrets...

3 Answers
1

I had this same issue with ECS Compose-X where I didn't really know where to change it. Originally thought of having a secret that would use {{resolve:secrets-manager:}} from the main RDS secret but that is going down the rabbit hole of potentially having the main secret rotate, and changes not reflect in the "fork" secret etc, which is a pain.

I much rather recommend in your docker image entrypoint to have a

if [ -z ${VAR_EXISTS} ] ; then DATABASE_URL=${VAR_EXISTS}; fi

That way, if you expose all the elements to build your connection string (DATABASE_URL) env var, you expose it, if not you don't. Then in your application do the same logic (if you can) -> is DATABASE_URL set ? if not, try to import all the bits; if not ; fail.

Another alternative is to have a sidecar container which will pull the info from the secrets and dump it into a volume shared between the two containers of your task definition. Have the first one put together an env file, and then again, add in your other container entrypoint to do source ._secret_env_file (or alike) and off you go. Have a look at Files Composer if you want a sidecar "ready to go" that will do what I just mentioned.

Hope this helps.

profile picture
answered 2 years ago
  • Just got the {{resolve:secrets-manager:}} thing working, but you're probably right that it's a path to pain...

    I was really hoping not to have to change the image but that's probably the pragmatic solution.

  • I'm accepting my own answer just because the original question was explicitly about not changing the image, and about safely using secrets to generate secrets. But I agree with you that your way is more desirable.

  • Also - ECS Compose-X looks exciting! I've been imagining a way to push a docker-compose straight to AWS but hadn't managed to find it.

1

I much prefer !Sub in general, but !Join is a great one too.

SecretString: !Sub
 - "postgres://${USERNAME}:${PASSWORD}@${HOSTNAME}:${PORT}"
 - USERNAME: !Sub ${{resolve:resolve:secretsmanager:${SECRET}:SecretString:username::}} #1
   PASSWORD: !Sub ${{resolve:resolve:secretsmanager:${SECRET}:SecretString:password::}}
   HOST: !Sub ${{resolve:resolve:secretsmanager:${SECRET}:SecretString:host::}} #2
   PORT: !Sub ${{resolve:resolve:secretsmanager:${SECRET}:SecretString:port::}} #2

#1 -> This will put the Arn of the secret in the resolve before resolve occurs

#2 -> alternatively, just use !GetAtt on the cluster return values

Personally find this more readable than the !Join CDK generated, but they are totally equivalent.

profile picture
answered 2 years ago
  • That's cool - how easy is it to generate that using the CDK?

0
Accepted Answer

With the major proviso that as @JohnPreston points out it will not pick up changes to the value of the original secret, it is possible to generate a new secret from an existing one as so:

DatabaseInstance dbInstance = makeDbInstance();
ISecret dbCredentials = dbInstance.getSecret();
Secret.Builder.create(stack, "name")
  .secretName("db-url")
  .secretStringValue(
    SecretValue.unsafePlainText(
      "{\"url\": \"postgres://" + dbCredentials.secretValueFromJson("username") + ":" + dbCredentials.secretValueFromJson("password") + "@" + dbCredentials.secretValueFromJson("host") + ":" + dbCredentials.secretValueFromJson("port") + "/mydbname\"}"
    )
  )
  .build();

It seems to be safe - the generated CloudFormation yaml is this:

  dburl5B3B78EC:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: db-url
      SecretString:
        Fn::Join:
          - ""
          - - |-
              {
                "url": "postgres://{{resolve:secretsmanager:
            - Ref: dbSecretAttachmentCCB3B2FC
            - ":SecretString:username::}}:{{resolve:secretsmanager:"
            - Ref: dbSecretAttachmentCCB3B2FC
            - ":SecretString:password::}}@{{resolve:secretsmanager:"
            - Ref: dbSecretAttachmentCCB3B2FC
            - ":SecretString:host::}}:{{resolve:secretsmanager:"
            - Ref: dbSecretAttachmentCCB3B2FC
            - |-
              :SecretString:port::}}/mydbname"
              }

so no exposed secrets there and they seem to resolve correctly in the new secret.

Rob
answered 2 years ago
  • Oh yeah, using {{resolve}} works great and is very safe. Make it even safer with setting a resource policy on the secret denying "normal users/roles" (humans really) from GetSecretValue on it, and create your stack with a CFN IAM Role. It will be able to CRUD the secret without ever letting someone retrieve it. And if you are happy with humans retrieving it, just make sure to log that in CloutTrail :)

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions