KMS Customer Managed Key with Grants and Aliases
Scenario
Your organisation's security policy requires that all S3 buckets, EBS volumes, and RDS instances use customer-managed KMS keys (CMKs) — not AWS-managed keys — so that your security team controls key access and can audit all encryption/decryption operations. You also need automatic annual key rotation and separate keys per environment.
Create a reusable KMS key configuration that serves as a template for all your encryption needs.
- Create an aws_kms_key with automatic rotation, a 30-day deletion window, and a full key policy.
- The key policy must: allow the account root, allow specific IAM roles to use the key, and allow specific roles to manage the key.
- Create two aliases: one generic (alias/app-encryption) and one environment-specific.
- Create a KMS grant allowing a Lambda function's IAM role to perform Decrypt operations.
- Output the key ARN, key ID, and both aliases.
Additional Context
KMS key policies and grants serve different purposes: Key policies are IAM-style JSON documents attached to the key and control who can manage it. Grants are temporary, programmatic permissions that a key administrator creates so specific principals (like Lambda functions) can use the key without needing an explicit IAM policy change. Grants are useful for automated cross-account workflows.
The multi_region = false setting creates a single-region key. Setting it to true creates a primary key that can be replicated to other regions for cross-region decryption — required for DR scenarios with encrypted cross-region data.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "environment" { type = string; default = "prod" }
variable "key_admin_arn" { type = string; description = "IAM role ARN that can manage this KMS key." }
variable "key_user_arns" { type = list(string); description = "IAM role ARNs that can use (encrypt/decrypt) the key." }
variable "lambda_role_arn"{ type = string; description = "IAM role ARN for the Lambda that gets a decrypt grant." }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
data "aws_caller_identity" "current" {}
# Full KMS key policy.
data "aws_iam_policy_document" "kms_policy" {
# CRITICAL: always grant root access so account admins cannot be locked out.
statement {
sid = "EnableRootAccess"
effect = "Allow"
principals { type = "AWS"; identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] }
actions = ["kms:*"]
resources = ["*"]
}
# Key administrators: can manage the key (rotate, schedule deletion, add grants).
# They CANNOT use the key for data encryption/decryption unless also listed in a use statement.
statement {
sid = "AllowKeyAdministration"
effect = "Allow"
principals { type = "AWS"; identifiers = [var.key_admin_arn] }
actions = [
"kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*",
"kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*",
"kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
]
resources = ["*"]
}
# Key users: can encrypt and decrypt data using this key.
statement {
sid = "AllowKeyUsage"
effect = "Allow"
principals { type = "AWS"; identifiers = var.key_user_arns }
actions = [
"kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*",
"kms:GenerateDataKey*", "kms:DescribeKey"
]
resources = ["*"]
}
# Allow grants — needed for services like Lambda to create grants programmatically.
statement {
sid = "AllowGrantCreation"
effect = "Allow"
principals { type = "AWS"; identifiers = var.key_user_arns }
actions = ["kms:CreateGrant", "kms:ListGrants", "kms:RevokeGrant"]
resources = ["*"]
condition {
test = "Bool"
variable = "kms:GrantIsForAWSResource"
values = ["true"]
}
}
}
# The KMS Customer Managed Key.
resource "aws_kms_key" "app" {
description = "Application encryption key — ${var.environment}"
deletion_window_in_days = 30 # 30-day soft delete window (max is 30)
enable_key_rotation = true # AWS rotates the backing key material annually
multi_region = false # single-region key; set true for DR setups
policy = data.aws_iam_policy_document.kms_policy.json
tags = { Environment = var.environment, ManagedBy = "terraform", Purpose = "app-encryption" }
}
# Primary alias — short and used in application configuration.
resource "aws_kms_alias" "app" {
name = "alias/app-encryption-${var.environment}"
target_key_id = aws_kms_key.app.key_id
}
# KMS Grant — gives the Lambda role decrypt permission without a key policy change.
# Grants are useful for ephemeral or cross-account access patterns.
resource "aws_kms_grant" "lambda_decrypt" {
name = "lambda-decrypt-grant"
key_id = aws_kms_key.app.key_id
grantee_principal = var.lambda_role_arn
operations = ["Decrypt", "DescribeKey"] # minimal permissions
}outputs.tf
output "kms_key_arn" { value = aws_kms_key.app.arn }
output "kms_key_id" { value = aws_kms_key.app.key_id }
output "kms_alias_name" { value = aws_kms_alias.app.name }
output "grant_id" { value = aws_kms_grant.lambda_decrypt.grant_id }Workflow Commands
terraform init
terraform apply \
-var="key_admin_arn=arn:aws:iam::123456789012:role/SecurityAdmin" \
-var='key_user_arns=["arn:aws:iam::123456789012:role/AppRole"]' \
-var="lambda_role_arn=arn:aws:iam::123456789012:role/LambdaRole"
# Encrypt a test value using the key alias
aws kms encrypt \
--key-id $(terraform output -raw kms_alias_name) \
--plaintext "Hello, KMS!" \
--query CiphertextBlob --output text | base64 -d > encrypted.bin
# List grants on the key
aws kms list-grants --key-id $(terraform output -raw kms_key_id)
terraform destroy -var="key_admin_arn=..." -var='key_user_arns=["..."]' -var="lambda_role_arn=..."Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: grant_id = "0ab1c2d3e4f5678901234567890123456789abcdef0123456789abcdef01234567" kms_alias_name = "alias/app-encryption-prod" kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" kms_key_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

