AWS Security Blog

How to automate replication of secrets in AWS Secrets Manager across AWS Regions

May 28, 2019: To correct a small bug, we’ve updated line 41 of the Python script provided below.


Assume that you make snapshot copies or read-replicas of your RDS databases in a secondary or backup AWS Region as a best practice. By using AWS Secrets Manager, you can store your RDS database credentials securely using AWS KMS Customer Master Keys, otherwise known as CMKs. AWS Key Management Service (AWS KMS) ensures secrets are encrypted at rest. With the integration of AWS Lambda, you can now more easily rotate these credentials regularly and replicate them for disaster recovery situations. This automation keeps credentials stored in AWS Secrets Manager for Amazon Relational Database Service (Amazon RDS) in sync between the origin Region, where your AWS RDS database lives, and the replica Region where your read-replicas live. While using the same credentials for all databases is not ideal, in the instance of disaster recovery, it can be useful for a quicker recovery.

In this post, I show you how to set up secret replication using an AWS CloudFormation template to create an AWS Lambda Function. By replicating secrets across AWS Regions, you can reduce the time required to get back up and running in production in a disaster recovery situation by ensuring your credentials are securely stored in the replica Region as well.

Solution overview

The solution described in this post uses a combination of AWS Secrets Manager, AWS CloudTrail, Amazon CloudWatch Events, and AWS Lambda. You create a secret in Secrets Manager that houses your RDS database credentials. This secret is encrypted using AWS KMS. Lambda automates the replication of the secret’s value in your origin AWS Region by performing a PUT operation on a secret of the same name in the same AWS Region as your read-replica. CloudWatch Events ensures that each time the secret housing your AWS RDS database credentials is rotated, it triggers the Lambda function to copy the secret’s value to your read-replica Region. By doing this, your RDS database credentials are always in sync for recovery.

Note: You might incur charges for using the services used in this solution, including Lambda. For information about potential costs, see the AWS pricing page.

The following diagram illustrates the process covered in this post.
 

Figure 1: Process diagram

Figure 1: Process diagram

This process assumes you have already created a secret housing your RDS database credentials in your main AWS Region and configured your CloudTrail Logs to send to CloudWatch Logs. Once this is complete, the steps to replicate are here:

  1. Secrets Manager rotates a secret in your original AWS Region.
  2. CloudTrail receives a log with “eventName”: “RotationSuceeded”.
  3. CloudTrail passes this log to CloudWatch Events.
  4. A filter in CloudWatch Events for this EventName triggers a Lambda function.
  5. The Lambda function retrieves the secret value from the origin AWS Region.
  6. The Lambda function then performs PutSecretValue on a secret with the same name in the replica AWS Region.

The Lambda function is triggered by a CloudWatch Event passed by CloudTrail. The triggering event is raised whenever a secret successfully rotates, which creates a CloudTrail log with the EventName property set to RotationSucceeded. You will know the secret rotation was successful when it has the label AWSCURRENT. You can read more about the secret labels and how they change during the rotation process here. The Lambda function retrieves the new secret, then calls PutSecretValue on a secret with the same name in the replica AWS Region. This AWS Region is specified by an environment variable inside the Lambda function.

Note: If the origin secret uses a customer-managed Customer Master Key (CMK), then the cloned secret must, as well. If the origin secret uses an AWS-managed CMK, then the cloned secret must, as well. You can’t mix them or the Lambda function will fail. AWS recommends you use customer-managed CMKs because you have full control of the permissions regarding which entities can use the CMK.

The CloudFormation template also creates an AWS Identity and Access Management (IAM) role with the required permissions to create and update secret replicas. Next, you’ll launch the CloudFormation template by using the AWS CloudFormation CLI.

Deploy the solution

Now that you understand how the Lambda function copies your secrets to the replica AWS Region, I’ll explain the commands to launch your CloudFormation stack. This stack creates the necessary resources to perform the automation. It includes the Lambda function, an IAM role, and the CloudWatch Event trigger.

First, make sure you have credentials for an IAM user or role that can launch all resources included in this template configured on your CLI. To launch the template, run the command below. You’ll choose a unique Stack Name to easily identify its purpose and the URL of the provided template you uploaded to your own S3 Bucket. For the following examples, I will use US-EAST-1 as my origin Region, and EU-WEST-1 as my replica Region. Make sure to replace these values and the other variables (identified by red font) with actual values from your own account.


$ aws cloudformation create-stack --stack-name Replication_Stack --template-url S3_URL --parameters ParameterKey=ReplicaKmsKeyArn,ParameterValue=arn:aws:kms:eu-west-1:111122223333:key/Example_Key_ID_12345 ParameterKey=TargetRegion,ParameterValue=eu-west-1 --capabilities CAPABILITY_NAMED_IAM –-region us-east-1

After the previous command is successful, you will see an output similar to the following with your CloudFormation Stack ARN:


$ {
    "StackId": "arn:aws:cloudformation:us-east-1:111122223333:stack/Replication_Stack/Example_additional_id_0123456789"
}

You can verify that the stack has completed successfully by running the following command:


$ aws cloudformation describe-stacks --stack-name Replication_Stack --region us-east-1

Verify that the StackStatus shows CREATE_COMPLETE. After you verify this, you’ll see three resources created in your account. The first is the IAM role, which allows the Lambda function to perform its API calls. The name of this role is SecretsManagerRegionReplicatorRole, and can be found in the IAM console under Roles. There are two policies attached to this role. The first policy is the managed permissions policy AWSLambdaBasicExecutionRole, which grants permissions for the Lambda function to write to AWS CloudWatch Logs. These logs will be used further on in the Event Rule creation, which will trigger the cloning of the origin secret to the replication region.

The second policy attached to the SecretsManagerRegionReplicatorRole role is an inline policy that grants permissions to decrypt and encrypt the secret in both your original AWS Region and in the replica AWS Region. This policy also grants permissions to retrieve the secret from the original AWS Region, and to store the secret in the replica AWS Region. You can see an example of this policy granting access to specific secrets below. Should you choose to use this policy, please remember to place your parameters into the placeholder values.


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "KMSPermissions",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt",
                "kms:Encrypt",
                "kms:GenerateDataKey"
            ],
            "Resource": [
          "arn:aws:kms:us-east-1:111122223333:key/Example_Key_ID_12345",
     "arn:aws:kms:eu-west-1:111122223333:key/Example_Key_ID_12345"
      ]
        },
        {
            "Sid": "SecretsManagerOriginRegion",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:DescribeSecret",
                "secretsmanager:GetSecretValue"
            ],
            "Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:replica/myexamplereplica*"
        },
        {
            "Sid": "SecretsManagerReplicaRegion",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:CreateSecret",
                "secretsmanager:UpdateSecretVersionStage",
                "secretsmanager:PutSecretValue",
                "secretsmanager:DescribeSecret"
            ],
            "Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:replica/myexamplereplica*"
        }
    ]
}

The next resource created is the CloudWatch Events rule SecretsManagerCrossRegionReplicator. You can find this rule by going to the AWS CloudWatch console, and, under Events, selecting Rules. Here’s an example Rule for the EventName I described earlier that’s used to trigger the Lambda function:


{
    "detail-type": [
        "AWS Service Event via CloudTrail"
    ],
    "source": [
        "aws.secretsmanager"
    ],
    "detail": {
        "eventSource": [
            "secretsmanager.amazonaws.com"
        ],
        "eventName": [
            "RotationSucceeded"
        ]
    }
}

The last resource created by the CloudFormation template is the Lambda function, which will do most of the actual work for the replication. After it’s created, you’ll be able to find this resource in your Lambda console with the name SecretsManagerRegionReplicator. You can download a copy of the Python script here, and you can see the full script below. In the function’s environment variables, you’ll also notice the parameter names and values you entered when launching your stack.


import boto3
from os import environ

targetRegion = environ.get('TargetRegion')
if targetRegion == None:
    raise Exception('Environment variable "TargetRegion" must be set')

smSource = boto3.client('secretsmanager')
smTarget = boto3.client('secretsmanager', region_name=targetRegion)

def lambda_handler(event, context):
    detail = event['detail']

    print('Retrieving SecretArn from event data')
    secretArn = detail['additionalEventData']['SecretId']

    print('Retrieving new version of Secret "{0}"'.format(secretArn))
    newSecret = smSource.get_secret_value(SecretId = secretArn)

    secretName = newSecret['Name']
    currentVersion = newSecret['VersionId']

    replicaSecretExists = True
    print('Replicating secret "{0}" (Version {1}) to region "{2}"'.format(secretName, currentVersion, targetRegion))
    try:
        smTarget.put_secret_value(
            SecretId = secretName,
            ClientRequestToken = currentVersion,
            SecretString = newSecret['SecretString']
        )
        pass
    except smTarget.exceptions.ResourceNotFoundException:
        print('Secret "{0}" does not exist in target region "{1}". Creating it now with default values'.format(secretName, targetRegion))
        replicaSecretExists = False
    except smTarget.exceptions.ResourceExistsException:
        print('Secret version "{0}" has already been created, this must be a duplicate invocation'.format(currentVersion))
        pass

    if replicaSecretExists == False:
        secretMeta = smSource.describe_secret(SecretId = secretArn)
        if 'KmsKeyId' in secretMeta:
            replicaKmsKeyArn = environ.get('ReplicaKmsKeyArn')
            if replicaKmsKeyArn == None:
                raise Exception('Cannot create replica of a secret that uses a custom KMS key unless the "ReplicaKmsKeyArn" environment variable is set. Alternatively, you can also create the key manually in the replica region with the same name')

            smTarget.create_secret(
                Name = secretName,
                ClientRequestToken = currentVersion,
                KmsKeyId = replicaKmsKeyArn,
                SecretString = newSecret['SecretString'],
                Description = secretMeta['Description']
            )
        else:
            smTarget.create_secret(
                Name = secretName,
                ClientRequestToken = currentVersion,
                SecretString = newSecret['SecretString'],
                Description = secretMeta['Description']
            )
    else:
        secretMeta = smTarget.describe_secret(SecretId = secretName)
        for previousVersion, labelList in secretMeta['VersionIdsToStages'].items():
            if 'AWSCURRENT' in labelList and previousVersion != currentVersion:
                print('Moving "AWSCURRENT" label from version "{0}" to new version "{1}"'.format(previousVersion, currentVersion))
                smTarget.update_secret_version_stage(
                    SecretId = secretName,
                    VersionStage = 'AWSCURRENT',
                    MoveToVersionId = currentVersion,
                    RemoveFromVersionId = previousVersion
                )
                break

    print('Secret {0} replicated successfully to region "{1}"'.format(secretName, targetRegion))

Now that your CloudFormation Stack is completed, and all necessary resources set up, you are ready to begin secret replication. To verify the setup works, you can modify the secret in the origin region that houses your RDS database credentials. It will take a few moments for the secret label to return to AWSCURRENT, and then roughly another 5-15 minutes for the CloudTrail Logs to populate and then send the Events to CloudWatch Logs. Once received, the Event will trigger the Lambda function to complete the replication process. You will be able to verify the secret has correctly replicated by going to the Secrets Manager Console in your replication Region, selecting the replicated secret, and viewing the values. If the values are the same in the replicated secret as they are the origin secret, and both labels are AWSCURRENT, you know replication completed successfully and your credentials will be ready if needed.

Summary

In this post, you learned how you can use AWS Lambda and Amazon CloudWatch Events to automate replication of your secrets in AWS Secrets Manager across AWS Regions. You used a CloudFormation template to create the necessary resources for the replication setup. The CloudWatch Event will watch for any CloudTrail Logs which would trigger the Lambda function that then pulls the secret name and value and replicates it to the AWS Region of your choice. Should a disaster occur, you’ll have increased your chances for a smooth recovery of your databases, and you’ll be back in production quicker.

If you have feedback about this blog post, submit comments in the Comments section below. If you have questions about this blog post, start a new thread on the AWS Secrets Manager forum.

Want more AWS Security news? Follow us on Twitter.

Author

Tracy Pierce

Tracy Pierce is a Senior Cloud Support Engineer at AWS. She enjoys the peculiar culture of Amazon and uses that to ensure every day is exciting for her fellow engineers and customers alike. Customer Obsession is her highest priority and she shows this by improving processes, documentation, and building tutorials. She has her AS in Computer Security & Forensics from SCTD, SSCP certification, AWS Developer Associate certification, and AWS Security Specialist certification. Outside of work, she enjoys time with friends, her Great Dane, and three cats. She keeps work interesting by drawing cartoon characters on the walls at request.