Terraform provides first-class support for AWS Key Management Service keys and key policies, but the interaction between immutable key resources, policy documents, and plan-time reads creates operational patterns that require explicit handling. The following guide synthesizes current practices for updating existing KMS key policies, avoiding plan-time access errors, restricting default key access, using community modules, and generating dynamic policies.
Updating Existing KMS Key Policies Without Re-creation
Terraform does not directly support updating an existing KMS key policy through its resources by default. The aws_kms_key resource manages the key itself, while aws_kms_key_policy manages the policy document attached to a key. When the key already exists outside of Terraform state, two approaches are commonly discussed.
The data source approach begins by fetching existing key information and the current policy, then building an updated policy in memory:
```hcl
data "awskmskey" "existingkey" {
keyid = "your-key-id-or-arn"
}
data "awskmskeypolicy" "existingpolicy" {
keyid = data.awskmskey.existingkey.id
}
locals {
updatedpolicy = jsonencode({
Version = "2012-10-17"
Statement = concat(
jsondecode(data.awskmskeypolicy.existing_policy.policy).Statement,
[
{
Sid = "NewStatement"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::123456789012:user/NewUser"
}
Action = ["kms:Encrypt", "kms:Decrypt"]
Resource = "*"
}
]
)
})
}
resource "awskmskeypolicy" "updatedpolicy" {
keyid = data.awskmskey.existingkey.id
policy = local.updated_policy
}
```
This pattern preserves existing statements and appends new ones. The alternative is to import the existing key into Terraform state and manage it with aws_kms_key with the policy argument. Import requires first running terraform import for the key, then providing a policy argument that matches the current key policy to avoid drift.
Reading KMS Keys During Terraform Plan and Access Errors
Plan-time reads of KMS keys can fail when Terraform attempts to resolve attributes used by dependent resources. One reported error occurs when Terraform tries to read the kms_key_id for an aws_secretsmanager_secret. The error manifests as:
Error: reading Secrets Manager Secret Version (arn:aws:secretsmanager:ap-northeast1:11111111:secret:hoge|terraform-111111111111111111111111): operation error Secrets Manager: GetSecretValue, https response error StatusCode: 400, RequestID: xxxxxxxxxxxxxx, api error AccessDeniedException: Access to KMS is not allowed
The same failure pattern appears when trying to read aws_kms_key.kms.key_id. The error is an AccessDeniedException for KMS access during Secrets Manager GetSecretValue. The issue stems from insufficient KMS permissions for the identity executing Terraform, not from Terraform configuration syntax.
Restricting Default Access to KMS via Key Policy
The default KMS key policy generated by AWS allows the account root to control key access via IAM policies. The default statement is:
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:",
"Resource": ""
}
By default KMS policy allow caller's account to use IAM policy to control key access. The Effect and Principal elements do not refer to the AWS root user account in isolation. Instead, it allows any principal in AWS account 123456789012 to have root access to the KMS key as long as you have attached the required permissions to the IAM entity.
A hardened Terraform blueprint replaces this with a custom policy that restricts delegation. The example policy includes three statements:
- Enable root access and prevent permission delegation
- Allow access for key administrators
- Enable read access to all identities
The first statement is:
{
"Sid": "Enable root access and prevent permission delegation",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:",
"Resource": "",
"Condition": {
"StringEquals": {
"aws:PrincipalType": "Account"
}
}
}
The second statement allows principals role ADMIN and TERRAFORM to perform management operations:
{
"Sid": "Allow access for key administrators",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:role/TERRAFORM",
"arn:aws:iam::123456789012:role/ADMIN"
]
},
"Action": [
"kms:Create",
"kms:Describe",
"kms:Enable",
"kms:List",
"kms:Put",
"kms:Update",
"kms:Revoke",
"kms:Disable",
"kms:Get",
"kms:Delete",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
}
The third statement enables read access to all identities:
{
"Sid": "Enable read access to all identities",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": [
"kms:List",
"kms:Get",
"kms:Describe"
],
"Resource": ""
}
The key policy allows the following permissions:
- First statement: The AWS root user account has full access to the key.
- Second statement: The principals role ADMIN and TERRAFORM has access to perform management operations on the key.
Note: This post demonstrates the AWS account ID 123456789012 with existing role named TERRAFORM, ADMIN and ANALYST. These values must be replaced for your environment.
Terraform AWS KMS Module Patterns
The Terraform module which creates AWS KMS resources provides reusable patterns for key usage, administrators, and aliases. 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"
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 = ["mycompany/ebs"]
tags = {
Terraform = "true"
Environment = "dev"
}
}
Reference usage for external CMK with 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"
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 = ["mycompany/external"]
aliases_use_name_prefix = true
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 =
}
}
See examples directory for working examples to reference.
Terraform KMS Resources and Data Sources Overview
Terraform AWS provider exposes a set of KMS resources and data sources.
- awskmsalias
ResourceManages an Kms Alias resource. - awskmsciphertext
ResourceManages an Kms Ciphertext resource. - awskmscustomkeystore
ResourceManages an Kms Custom Key Store resource. - awskmsexternal_key
ResourceManages an Kms External Key resource. - awskmsgrant
ResourceManages an Kms Grant resource. - awskmskey
ResourceManages an Kms Key resource. - awskmskey_policy
ResourceManages an Kms Key Policy resource. - awskmsreplicaexternalkey
ResourceManages an Kms Replica External Key resource. - awskmsreplica_key
ResourceManages an Kms Replica Key resource.
The provider also lists 7 data sources available, complementing the 9 resources above.
The following table summarizes the core resource types for KMS policy management:
| Resource | Purpose |
| awskmskey | Manages a KMS key resource |
| awskmskeypolicy | Manages a KMS key policy resource |
| awskmsalias | Manages a KMS alias resource |
| awskms_grant | Manages a KMS grant resource |
Dynamic Policy Generation with Templates
From static to dynamic: building flexible KMS key policies with Terraform's template magic. The challenge is managing KMS keys across multiple environments where dev, staging, prod each need different permissions. Developers need limited access for debugging, S3 buckets require decrypt permissions, databases have their own encryption needs, cross-account access for partner integrations.
The old-school approach uses hardcoded and inflexible templatefile:
```hcl
resource "awskmskey" "this" {
description = var.description
policy = templatefile("${path.module}/templates/${var.policy_template}.tpl", {
description = var.description
})
}
variable "policytemplate" {
default = "generickey_template"
}
```
This works until it does not. The static reference limits reuse.
Conclusion
Managing KMS keys with Terraform requires separating key lifecycle from policy lifecycle, handling plan-time KMS read permissions, and deliberately restricting the default permissive root policy. Updating an existing key policy is best achieved by reading the current policy with aws_kms_key_policy data source, merging statements locally with jsonencode and concat, and applying via aws_kms_key_policy resource. Importing existing keys allows full aws_kms_key management when ownership is transferred to Terraform.
Access denied errors during plan often trace to Secrets Manager or other resources needing KMS decryption permissions for the executing principal. Restricting default access involves replacing the broad root allow with conditions such as aws:PrincipalType equals Account and explicit administrator roles for management operations.
Community modules provide reusable patterns for key usage, administrators, aliases, and grants, including external CMK support. The provider offers nine resources and seven data sources covering aliases, ciphertext, custom key stores, external keys, grants, keys, key policies, and replicas.
Dynamic policy generation with templatefile and variables enables environment-specific policies without hardcoding, supporting modular, self-discovering infrastructure across dev, staging, and prod.