Terraform provides declarative control over AWS Key Management Service customer managed keys, key policies, aliases, rotation, and grants. The reference material covers creation of new symmetric keys, safe deletion windows, alias management, module-based provisioning, and the limitations around updating existing key policies. Updating an existing key policy is not natively supported by the aws_kms_key resource and requires data sources, imports, or external execution.
Creating Customer Managed KMS Keys With Terraform
AWS Key Management Service is the foundation of encryption on AWS. Many AWS encryption features use KMS behind the scenes, including SSE-KMS for S3 objects, encrypted RDS databases, encrypted EBS volumes, and Secrets Manager secrets. Most of the time you can use AWS-managed keys and not think about it. But when you need fine-grained control over key policies, rotation, or cross-account access, you need customer-managed keys.
This post covers creating and managing KMS keys with Terraform, including key policies, aliases, automatic rotation, and multi-region setups.
A basic symmetric encryption key is the most common type.
hcl
resource "aws_kms_key" "main" {
description = "Main encryption key for application data"
deletion_window_in_days = 30
enable_key_rotation = true
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
Key alias for easier reference
hcl
resource "aws_kms_alias" "main" {
name = "alias/myapp-main"
target_key_id = aws_kms_key.main.key_id
}
Outputs can be exported for other configurations:
```hcl
output "kmskeyid" {
description = "The KMS key ID"
value = awskmskey.main.key_id
}
output "kmskeyarn" {
description = "The KMS key ARN"
value = awskmskey.main.arn
}
output "kmsaliasarn" {
description = "The KMS alias ARN"
value = awskmsalias.main.arn
}
```
Key Configuration Options and Safety Defaults
Deletion window is a safety net. Once a key is deleted, anything encrypted with it is gone forever. Always set this to the maximum for production keys.
Important notes from the reference:
deletion_window_in_dayssets a waiting period before the key is permanently deleted- The minimum deletion window is 7 days
- Always use the maximum of 30 days for production keys
- Better yet, disable the key first and wait before scheduling deletion
Key rotation can be enabled at creation:
hcl
enable_key_rotation = true
Tags are commonly applied for ownership and environment tracking.
The following table summarizes common key attributes referenced:
| Attribute | Example Value | Notes |
|---|---|---|
| description | Main encryption key for application data | Human readable description |
| deletionwindowin_days | 30 | Maximum recommended for production |
| enablekeyrotation | true | Automatic rotation |
| tags | Environment = production | ManagedBy = terraform |
Updating Existing KMS Key Policies
Terraform doesn't directly support updating an existing KMS key policy through its resources. There are a few approaches you can consider to achieve this.
Data Source Approach for Policy Modification
First, use the aws_kms_key data source to get information about the existing key:
hcl
data "aws_kms_key" "existing_key" {
key_id = "your-key-id-or-arn"
}
Then, use the aws_kms_key_policy data source to fetch the current policy:
hcl
data "aws_kms_key_policy" "existing_policy" {
key_id = data.aws_kms_key.existing_key.id
}
Next, create a local variable to modify the policy:
hcl
locals {
updated_policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
jsondecode(data.aws_kms_key_policy.existing_policy.policy).Statement,
[
{
Sid = "NewStatement"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::123456789012:user/NewUser"
}
Action = ["kms:Encrypt", "kms:Decrypt"]
Resource = "*"
}
]
)
})
}
Finally, use the aws_kms_key_policy resource to apply the updated policy:
hcl
resource "aws_kms_key_policy" "updated_policy" {
key_id = data.aws_kms_key.existing_key.id
policy = local.updated_policy
}
Import Existing Key With Policy Argument
If you need more control or if the above method doesn't work for your use case, you can use the aws_kms_key resource with the policy argument, but you'll need to import the existing key first:
hcl
resource "aws_kms_key" "existing_key" {
description = "Existing KMS key"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# Your existing policy statements here
{
Sid = "NewStatement"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::123456789012:user/NewUser"
}
Action = ["kms:Encrypt", "kms:Decrypt"]
Resource = "*"
}
]
})
}
Then import the existing key:
bash
terraform import aws_kms_key.existing_key your-key-id-or-arn
Null Resource Fallback With AWS CLI
As a last resort, you could use a null_resource with local-exec to run AWS CLI commands to update the policy.
```hcl
resource "nullresource" "updatekmspolicy" {
provisioner "local-exec" {
command = <
Use jq or another tool to modify existing_policy.json
aws kms put-key-policy --key-id ${yourkeyid} --policy-name default --policy file://updated_policy.json
EOF
}
```
This is not ideal from a Terraform perspective but can work if other methods fail.
Remember that these approaches may have limitations and might not be suitable for all scenarios. Always test thoroughly in a non-production environment before applying changes to your KMS key policies.
Module Based KMS Key Provisioning
The terraform-aws-modules/terraform-aws-kms module creates AWS KMS resources.
Reference usage for EC2 AutoScaling service linked role to launch encrypted EBS volumes:
hcl
module "kms" {
source = "terraform-aws-modules/kms/aws"
description = "EC2 AutoScaling key usage"
key_usage = "ENCRYPT_DECRYPT"
# Policy
key_administrators = ["arn:aws:iam::012345678901:role/admin"]
key_service_roles_for_autoscaling = ["arn:aws:iam::012345678901:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"]
# Aliases
aliases = ["mycompany/ebs"]
tags = {
Terraform = "true"
Environment = "dev"
}
}
Reference usage for external CMK externally provided encryption material:
hcl
module "kms" {
source = "terraform-aws-modules/kms/aws"
description = "External key example"
key_material_base64 = "Wblj06fduthWggmsT0cLVoIMOkeLbc2kVfMud77i/JY="
valid_to = "2085-04-12T23:20:50.52Z"
# Policy
key_owners = ["arn:aws:iam::012345678901:role/owner"]
key_administrators = ["arn:aws:iam::012345678901:role/admin"]
key_users = ["arn:aws:iam::012345678901:role/user"]
key_service_users = ["arn:aws:iam::012345678901:role/ec2-role"]
# Aliases
aliases = ["mycompany/external"]
aliases_use_name_prefix = true
# Grants
grants = {
lambda = {
grantee_principal = "arn:aws:iam::012345678901:role/lambda-function"
operations = ["Encrypt", "Decrypt", "GenerateDataKey"]
constraints = {
encryption_context_equals = {
Department = "Finance"
}
}
}
}
tags = {
Terraform = "true"
Environment =
}
}
CloudPosse KMS Key Module
Terraform module to provision a KMS key with alias. Can be used with chamber for managing secrets by storing them in Amazon EC2 Systems Manager Parameter Store.
Tip example:
hcl
module "kms_key" {
source = "cloudposse/kms-key/aws"
# Cloud Posse recommends pinning every module to a specific version
# version = "x.x.x"
namespace = "eg"
stage = "test"
name = "chamber"
description = "KMS key for chamber"
deletion_window_in_days = 10
enable_key_rotation = true
alias = "alias/parameter_store_key"
}
Important module requirements:
- terraform >= 0.13
- aws >= 3.64.0
| Name | Version |
|---|---|
| terraform | >= 0.13 |
| aws | >= 3.64.0 |
| Name | Source | Version |
|---|---|---|
| this | cloudposse/label/null | 0.25.0 |
| Name | Type |
|---|---|
| awskmsalias.default | resource |
| awskmskey.default | resource |
Module inputs include:
- additionaltagmap
- alias
- attributes
Common Mistakes and Operational Risks
Locking yourself out. If you remove the root account access from the key policy and no remaining principal can update the policy, you can't modify the key anymore. The only way to recover is to contact AWS support.
Forgetting grants. Some AWS services like EBS and RDS use grants instead of direct key policies. Make sure the roles that interact with these services have kms:CreateGrant permission.
Deleting keys too quickly. The minimum deletion window is 7 days. Always use the maximum of 30 days for production keys. Better yet, disable the key first and wait before scheduling deletion.
The following table summarizes common mistakes:
| Mistake | Impact | Mitigation |
|---|---|---|
| Removing root account access | Policy lockout, requires AWS support | Always include root account access in key policies |
| Forgetting grants | Service failures for EBS/RDS | Ensure roles have kms:CreateGrant permission |
| Short deletion window | Irrecoverable data loss | Use 30 days, disable before delete |
Wrapping Up
KMS keys are the backbone of encryption on AWS. Customer-managed keys give you control over who can access your encrypted data, audit trails through CloudTrail, and automatic key rotation. Create separate keys for different services, always include root account access in key policies, and never set the deletion window below 30 days for production. Encryption is only as strong as your key management, and Terraform makes it auditable.
Conclusion
Terraform enables consistent, versioned creation of customer managed KMS keys with controlled deletion windows, rotation, aliases, and tagging. New key creation follows a straightforward resource pattern with aws_kms_key and aws_kms_alias, and outputs can be shared across configurations.
Updating an existing key policy is more constrained. The recommended patterns are data source driven policy reconstruction with aws_kms_key_policy, importing the key into state and managing the policy argument, or a null resource with local-exec AWS CLI as a last resort. Each approach carries trade-offs in drift detection, state management, and safety.
Module usage via terraform-aws-modules/kms/aws and cloudposse/kms-key/aws abstracts policy composition, administrators, users, service roles, grants, and aliases into reusable parameters. These modules are useful for EC2 AutoScaling EBS encryption, external key material, and parameter store integration.
Operational safety remains paramount. Maintain root access, respect the 7 day minimum and 30 day recommended deletion window, understand grant based service interactions, and test policy changes in non-production before applying to production keys.