KMS multi-region keys
AWS Key Management Service (KMS) is a fully managed service that makes it easy to create and control the encryption keys used to encrypt your data.
One of the key features of KMS is its support for multi-region replication, which allows you to create and manage encryption keys across multiple regions.
Multi-region replication is a useful feature for organizations that operate in multiple regions or have a global presence. It allows you to use the same encryption keys across multiple regions, which simplifies the process of encrypting and decrypting data as it is transferred between regions. This can be particularly useful for organizations that need to frequently transfer sensitive data between regions, such as for backup and disaster recovery purposes.
One of the benefits of using KMS multi-region replication is that it allows you to have a consistent encryption strategy across all of your regions. This can make it easier to manage and enforce security policies, as you can use the same encryption keys and processes in all regions. It can also help to reduce the risk of data loss or exposure, as you can use the same encryption keys to protect data in all regions.
How to create a KMS multi-region key
To use KMS multi-region replication, you first need to create a key in one region, and then replicate it to one or more additional regions. Once a key has been replicated to a region, you can use it to encrypt and decrypt data in that region just like any other key.
There is no L2 CDK construct to create a KMS multi-region key yet, so we'll use the L1 constructs: CfnKey
and CfnReplicaKey
.
In this tutorial, we're going to create the multi-region key in the us-east-1 region (the primary key). The key will then be replicated in the eu-west-1 region (the replica key).
Creating the multi-region key
Let's first create a stack to create the KMS multi-region key:
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as kms from "aws-cdk-lib/aws-kms";
const createPolicy = (stack: cdk.Stack): any => {
return {
Statement: [
{
Sid: "Enable IAM policies",
Effect: "Allow",
Principal: {
AWS: `arn:${cdk.Stack.of(stack).partition}:iam::${
cdk.Stack.of(stack).account
}:root`,
},
Action: "kms:*",
Resource: "*",
},
],
};
};
export class PrimaryStack extends cdk.Stack {
public readonly primaryKeyArn: string;
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const primaryKey = new kms.CfnKey(this, "KmsPrimaryKey", {
keyPolicy: createPolicy(this),
multiRegion: true,
});
this.primaryKeyArn = primaryKey.attrArn;
}
}
The createPolicy
helper function generates a good default policy for the KMS key.
In order to create the multi-region key, we use the CfnKey
L1 construct and set the multiregion
property.
A multi-region KMS key ID always start with mrk-
.
We're also keeping a reference to the KMS key arn, since we'll gonna need it later.
Creating the replica key
Next, let's replicate the multi-region key in another stack:
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as kms from "aws-cdk-lib/aws-kms";
interface ReplicaStackProps extends cdk.StackProps {
primaryKeyArn: string;
}
export class ReplicaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ReplicaStackProps) {
super(scope, id, props);
const { primaryKeyArn } = props;
new kms.CfnReplicaKey(this, "KmsReplicaKey", {
primaryKeyArn,
keyPolicy: createPolicy(this),
});
}
}
We're using the CfnReplicaKey
L1 construct to replicate the kms key.
We're passing the primary key arn, as a StackProps
parameter.
Since the replica key will be created in the region the stack is created in, we'll need to create both stacks in different regions:
const { primaryKeyArn } = new PrimaryStack(app, "PrimaryStack", {
env: {
region: "us-east-1"
}
});
new ReplicaStack(app, "ReplicaStack", {
primaryKeyArn,
env: {
region: "eu-west-1",
},
However, when deploying the stacks, we get this error:
Error: Stack "ReplicaStack" cannot consume a cross reference from stack "PrimaryStack". Cross stack references are only supported for stacks deployed to the same environment or between nested stacks and their parent stack
How to pass a parameter from a stack in one region to another region
In order to pass a parameter from one stack to another, in different regions, using CloudFormation Outputs (CfnOutput
) or StackProps
won't work, as they can only be used between stacks in the same region.
What we can do instead is save the parameter to share in the System Manager Parameter Store:
new ssm.StringParameter(this, "PrimaryKeyArn", {
parameterName: "...",
description: "...",
stringValue: "...",
});
To retrieve the parameter, we can use a custom resource:
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
import * as iam from "aws-cdk-lib/aws-iam";
interface SSMParameterReaderProps {
parameterName: string;
region: string;
}
export class SSMParameterReader extends cr.AwsCustomResource {
constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
const { parameterName, region } = props;
const ssmAwsSdkCall: cr.AwsSdkCall = {
service: "SSM",
action: "getParameter",
parameters: {
Name: parameterName,
},
region,
physicalResourceId: { id: Date.now().toString() }, // Update physical id to always fetch the latest version
};
super(scope, name, {
onUpdate: ssmAwsSdkCall,
policy: {
statements: [
new iam.PolicyStatement({
resources: ["*"],
actions: ["ssm:GetParameter"],
effect: iam.Effect.ALLOW,
}),
],
},
});
}
public getParameterValue(): string {
return this.getResponseField("Parameter.Value").toString();
}
}
Passing the primary key ARN to the replica stack
Now we can update the stacks, so that ReplicaStack
uses the primary key ARN created in PrimaryStack
.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as kms from "aws-cdk-lib/aws-kms";
import * as ssm from "aws-cdk-lib/aws-ssm";
import { SSMParameterReader } from "./SSMParameterReader";
const PRIMARY_KEY_ARN = "PRIMARY_KEY_ARN";
const createPolicy = (stack: cdk.Stack): any => {
return {
Statement: [
{
Sid: "Enable IAM policies",
Effect: "Allow",
Principal: {
AWS: `arn:${cdk.Stack.of(stack).partition}:iam::${
cdk.Stack.of(stack).account
}:root`,
},
Action: "kms:*",
Resource: "*",
},
],
};
};
export class PrimaryStack extends cdk.Stack {
public readonly primaryKeyArn: string;
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const primaryKey = new kms.CfnKey(this, "KmsPrimaryKey", {
keyPolicy: createPolicy(this),
multiRegion: true,
});
this.primaryKeyArn = primaryKey.attrArn;
new ssm.StringParameter(this, "PrimaryKeyArn", {
parameterName: PRIMARY_KEY_ARN,
description: "The primary key ARN to be replicated",
stringValue: primaryKey.attrArn,
});
}
}
export class ReplicaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const primaryKeyArnSSMReader = new SSMParameterReader(
this,
"PrimaryKeyArnSSMReader",
{
parameterName: PRIMARY_KEY_ARN,
region: "us-east-1",
}
);
const primaryKeyArn = primaryKeyArnSSMReader.getParameterValue();
new kms.CfnReplicaKey(this, "KmsReplicaKey", {
primaryKeyArn,
keyPolicy: createPolicy(this),
});
}
}
And the stacks are created like so:
const app = new cdk.App();
new PrimaryStack(app, "PrimaryStack", {
env: {
region: "us-east-1",
},
});
new ReplicaStack(app, "ReplicaStack", {
env: {
region: "eu-west-1",
},
});
We can deploy both stacks:
npx aws-cdk deploy --all
Testing the KMS key
We can test our infrastructure by encrypting some data, then decrypting it in both regions, using the AWS CLI.
Encrypting data
To encrypt the content of a file, we'll use command:
aws kms encrypt \
--plaintext fileb://secret-message.txt \
--key-id <YOUR_MRK_KEY> \
--region us-east-1 \
--output text \
--query CiphertextBlob | base64 --decode > secret-message.enc
This command will encrypt the content of the secret-message.txt file into the secret-message.enc file. Notice that we're encrypting the data using the us-east-1 key.
Decrypting the data within the same region
To decrypt the data we can use this command:
aws kms decrypt \
--ciphertext-blob fileb://secret-message.enc \
--key-id <YOUR_MRK_KEY> \
--region us-east-1 \
--output text \
--query Plaintext | base64 --decode > secret-message-us-east-1.dec
Notice that we're decrypting the file using the same key, in the same region that we've use for encryption (us-east-1).
Decrypting the data within a region without replica key
Let's try to decrypt the data in ca-central-1, a region without a replica key:
aws kms decrypt \
--ciphertext-blob fileb://secret-message.enc \
--key-id <YOUR_MRK_KEY> \
--region ca-central-1 \
--output text \
--query Plaintext | base64 --decode > secret-message-ca-central-1.dec
Running this command generate the error:
An error occurred (AccessDeniedException) when calling the Decrypt operation: The ciphertext refers to a customer master key that does not exist, does not exist in this region, or you are not allowed to access.
This demosntrates that without replicating a key, we can't use it in another region.
Decrypting the data within a region with a replica key
We can verify that the replica key works by decrypting the data in eu-west-1, the region we created the replica key into:
aws kms decrypt \
--ciphertext-blob fileb://secret-message.enc \
--key-id <YOUR_MRK_KEY> \
--region eu-west-1 \
--output text \
--query Plaintext | base64 --decode > secret-message-eu-west-1.dec
Conclusion
AWS KMS multi-region replication is a useful feature for organizations that operate in multiple regions or have a global presence. It allows you to create and manage encryption keys across multiple regions, which simplifies the process of encrypting and decrypting data as it is transferred between regions.
By using KMS multi-region replication, you can have a consistent encryption strategy across all of your regions, which can help to reduce the risk of data loss or exposure and make it easier to manage and enforce security policies.
Source code available on github