Skip to content

Latest commit

 

History

History
295 lines (228 loc) · 10.4 KB

File metadata and controls

295 lines (228 loc) · 10.4 KB

AWS Secrets Manager Construct Library


cfn-resources: Stable

cdk-constructs: Stable


import * as secretsmanager from '@aws-cdk/aws-secretsmanager';

Create a new Secret in a Stack

To have SecretsManager generate a new secret value automatically, follow this example:

declare const vpc: ec2.Vpc;

// Simple secret
const secret = new secretsmanager.Secret(this, 'Secret');
// Using the secret
const instance1 = new rds.DatabaseInstance(this, "PostgresInstance1", {
  engine: rds.DatabaseInstanceEngine.POSTGRES,
  credentials: rds.Credentials.fromSecret(secret),
  vpc
});
// Templated secret with username and password fields
const templatedSecret = new secretsmanager.Secret(this, 'TemplatedSecret', {
  generateSecretString: {
    secretStringTemplate: JSON.stringify({ username: 'postgres' }),
    generateStringKey: 'password',
  },
});
// Using the templated secret as credentials
const instance2 = new rds.DatabaseInstance(this, "PostgresInstance2", {
  engine: rds.DatabaseInstanceEngine.POSTGRES,
  credentials: {
    username: templatedSecret.secretValueFromJson('username').toString(),
    password: templatedSecret.secretValueFromJson('password')
  },
  vpc
});

If you need to use a pre-existing secret, the recommended way is to manually provision the secret in AWS SecretsManager and use the Secret.fromSecretArn or Secret.fromSecretAttributes method to make it available in your CDK Application:

declare const encryptionKey: kms.Key;
const secret = secretsmanager.Secret.fromSecretAttributes(this, 'ImportedSecret', {
  secretArn: 'arn:aws:secretsmanager:<region>:<account-id-number>:secret:<secret-name>-<random-6-characters>',
  // If the secret is encrypted using a KMS-hosted CMK, either import or reference that key:
  encryptionKey,
});

SecretsManager secret values can only be used in select set of properties. For the list of properties, see the CloudFormation Dynamic References documentation.

A secret can set RemovalPolicy. If it set to RETAIN, removing that secret will fail.

Grant permission to use the secret to a role

You must grant permission to a resource for that resource to be allowed to use a secret. This can be achieved with the Secret.grantRead and/or Secret.grantWrite method, depending on your need:

const role = new iam.Role(this, 'SomeRole', { assumedBy: new iam.AccountRootPrincipal() });
const secret = new secretsmanager.Secret(this, 'Secret');
secret.grantRead(role);
secret.grantWrite(role);

If, as in the following example, your secret was created with a KMS key:

declare const role: iam.Role;
const key = new kms.Key(this, 'KMS');
const secret = new secretsmanager.Secret(this, 'Secret', { encryptionKey: key });
secret.grantRead(role);
secret.grantWrite(role);

then Secret.grantRead and Secret.grantWrite will also grant the role the relevant encrypt and decrypt permissions to the KMS key through the SecretsManager service principal.

The principal is automatically added to Secret resource policy and KMS Key policy for cross account access:

const otherAccount = new iam.AccountPrincipal('1234');
const key = new kms.Key(this, 'KMS');
const secret = new secretsmanager.Secret(this, 'Secret', { encryptionKey: key });
secret.grantRead(otherAccount);

Rotating a Secret

Using a Custom Lambda Function

A rotation schedule can be added to a Secret using a custom Lambda function:

import * as lambda from '@aws-cdk/aws-lambda';

declare const fn: lambda.Function;
const secret = new secretsmanager.Secret(this, 'Secret');

secret.addRotationSchedule('RotationSchedule', {
  rotationLambda: fn,
  automaticallyAfter: Duration.days(15),
});

Note: The required permissions for Lambda to call SecretsManager and the other way round are automatically granted based on AWS Documentation as long as the Lambda is not imported.

See Overview of the Lambda Rotation Function on how to implement a Lambda Rotation Function.

Using a Hosted Lambda Function

Use the hostedRotation prop to rotate a secret with a hosted Lambda function:

const secret = new secretsmanager.Secret(this, 'Secret');

secret.addRotationSchedule('RotationSchedule', {
  hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(),
});

Hosted rotation is available for secrets representing credentials for MySQL, PostgreSQL, Oracle, MariaDB, SQLServer, Redshift and MongoDB (both for the single and multi user schemes).

When deployed in a VPC, the hosted rotation implements ec2.IConnectable:

declare const myVpc: ec2.Vpc;
declare const dbConnections: ec2.Connections;
declare const secret: secretsmanager.Secret;

const myHostedRotation = secretsmanager.HostedRotation.mysqlSingleUser({ vpc: myVpc });
secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotation });
dbConnections.allowDefaultPortFrom(myHostedRotation);

Use the excludeCharacters option to customize the characters excluded from the generated password when it is rotated. By default, the rotation excludes the same characters as the ones excluded for the secret. If none are defined then the following set is used: % +~`#$&*()|[]{}:;<>?!'/@"\.

See also Automating secret creation in AWS CloudFormation.

Rotating database credentials

Define a SecretRotation to rotate database credentials:

declare const mySecret: secretsmanager.Secret;
declare const myDatabase: ec2.IConnectable;
declare const myVpc: ec2.Vpc;

new secretsmanager.SecretRotation(this, 'SecretRotation', {
  application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, // MySQL single user scheme
  secret: mySecret,
  target: myDatabase, // a Connectable
  vpc: myVpc, // The VPC where the secret rotation application will be deployed
  excludeCharacters: ' %+:;{}', // characters to never use when generating new passwords;
                                // by default, no characters are excluded,
                                // which might cause problems with some services, like DMS
});

The secret must be a JSON string with the following format:

{
  "engine": "<required: database engine>",
  "host": "<required: instance host name>",
  "username": "<required: username>",
  "password": "<required: password>",
  "dbname": "<optional: database name>",
  "port": "<optional: if not specified, default port will be used>",
  "masterarn": "<required for multi user rotation: the arn of the master secret which will be used to create users/change passwords>"
}

For the multi user scheme, a masterSecret must be specified:

declare const myUserSecret: secretsmanager.Secret;
declare const myMasterSecret: secretsmanager.Secret;
declare const myDatabase: ec2.IConnectable;
declare const myVpc: ec2.Vpc;

new secretsmanager.SecretRotation(this, 'SecretRotation', {
  application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_MULTI_USER,
  secret: myUserSecret, // The secret that will be rotated
  masterSecret: myMasterSecret, // The secret used for the rotation
  target: myDatabase,
  vpc: myVpc,
});

See also aws-rds where credentials generation and rotation is integrated.

Importing Secrets

Existing secrets can be imported by ARN, name, and other attributes (including the KMS key used to encrypt the secret). Secrets imported by name should use the short-form of the name (without the SecretsManager-provided suffix); the secret name must exist in the same account and region as the stack. Importing by name makes it easier to reference secrets created in different regions, each with their own suffix and ARN.

const secretCompleteArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9';
const secretPartialArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; // No Secrets Manager suffix
const encryptionKey = kms.Key.fromKeyArn(this, 'MyEncKey', 'arn:aws:kms:eu-west-1:111111111111:key/21c4b39b-fde2-4273-9ac0-d9bb5c0d0030');
const mySecretFromCompleteArn = secretsmanager.Secret.fromSecretCompleteArn(this, 'SecretFromCompleteArn', secretCompleteArn);
const mySecretFromPartialArn = secretsmanager.Secret.fromSecretPartialArn(this, 'SecretFromPartialArn', secretPartialArn);
const mySecretFromName = secretsmanager.Secret.fromSecretNameV2(this, 'SecretFromName', 'MySecret')
const mySecretFromAttrs = secretsmanager.Secret.fromSecretAttributes(this, 'SecretFromAttributes', {
  secretCompleteArn,
  encryptionKey,
});

Replicating secrets

Secrets can be replicated to multiple regions by specifying replicaRegions:

declare const myKey: kms.Key;
new secretsmanager.Secret(this, 'Secret', {
  replicaRegions: [
    {
      region: 'eu-west-1',
    },
    {
      region: 'eu-central-1',
      encryptionKey: myKey,
    }
  ]
});

Alternatively, use addReplicaRegion():

const secret = new secretsmanager.Secret(this, 'Secret');
secret.addReplicaRegion('eu-west-1');

Creating JSON Secrets

Sometimes it is necessary to create a secret in SecretsManager that contains a JSON object. For example:

{
  "username": "myUsername",
  "database": "foo",
  "password": "mypassword"
}

In order to create this type of secret, use the secretObjectValue input prop.

const user = new iam.User(stack, 'User');
const accessKey = new iam.AccessKey(stack, 'AccessKey', { user });
declare const stack: Stack;

new secretsmanager.Secret(stack, 'Secret', {
  secretObjectValue: {
    username: SecretValue.unsafePlainText(user.userName),
    database: SecretValue.unsafePlainText('foo'),
    password: accessKey.secretAccessKey,
  },
})

In this case both the username and database are not a Secret so SecretValue.unsafePlainText needs to be used. This means that they will be rendered as plain text in the template, but in this case neither of those are actual "secrets".