Business Challenge
A financial services company runs 40 AWS accounts under AWS Organizations. Their security review finds that the platform team created one KMS CMK in the management account, added it to every service in every account, and called the job done. S3 encryption: enabled. RDS encryption: enabled. Secrets Manager: encrypted. The auditors are happy.
Then they look at the blast radius. A compromised ECS task role in the payments account can call kms:Decrypt with that one key to read the HR account's S3 data, the logging account's CloudTrail exports, and the shared secrets in every account simultaneously. One key. One compromised role. Full organisational exposure.
The problem is not that encryption is enabled. The problem is that encryption without key isolation provides confidentiality at rest but not confidentiality between principals. If every principal that can read any ciphertext uses the same key, encryption is a compliance checkbox, not a security control.
The goal of a KMS key hierarchy is to ensure that a credential compromise in one account, team, or service cannot be used to decrypt data belonging to a different account, team, or service. Key segmentation is how you contain the blast radius when the inevitable credential leak happens.
Architecture
A well-designed KMS key hierarchy has three layers: a management account that holds centralised audit CMKs, per-account service CMKs for workload encryption, and a shared services CMK for the narrow set of data that genuinely crosses account boundaries.
Envelope encryption β what KMS actually does
KMS never encrypts your application data directly. It encrypts a data encryption key (DEK), a 256-bit symmetric key that your application or AWS service uses locally. This is envelope encryption. AWS services like S3, RDS, and EBS call GenerateDataKey, receive a plaintext DEK and an encrypted DEK, encrypt the data locally with the plaintext DEK, discard the plaintext DEK, and store the encrypted DEK alongside the ciphertext. To decrypt, they call Decrypt with the encrypted DEK and receive the plaintext DEK back.
The consequence: KMS scales to petabytes of data. Rotating a CMK does not re-encrypt all the data β only new DEKs use the new key material. Old ciphertexts remain decryptable because KMS stores previous key versions and uses the correct one when you call Decrypt with an encrypted DEK.
The key policy: the only authoritative control
Every KMS CMK has a key policy. Unlike S3 bucket policies or IAM policies, the key policy is the primary access control for a KMS key. IAM policies for KMS work differently from other AWS services: a principal needs permission in both the key policy and their IAM policy to use a key. The key policy can delegate IAM to manage access β but only if the key policy explicitly includes the account root with full access.
The mandatory statement every key policy must include:
{
"Sid": "Enable IAM policies",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "kms:*",
"Resource": "*"
}
Without this statement, IAM policies cannot grant any access to the key. If you lock yourself out by creating a key without it, you must contact AWS Support to recover the key β you cannot fix it yourself.
Why This Architecture Works
Blast radius containment
A compromised role in Account A can only decrypt data encrypted with Account A's CMKs. It has no access to keys in other accounts. Lateral movement requires compromising a key policy, which is separately audited and managed.
Separation of key admin and key user
The key administrator role can manage key policy and rotation but cannot use the key to encrypt or decrypt data. The key user role can encrypt and decrypt but cannot modify the key policy. These are separate IAM roles with separate policies.
Cross-account audit without cross-account access
Every Decrypt call against a key in Account A is logged in Account A's CloudTrail, including the caller identity from Account B. The key owner has a complete audit trail of who used their key, even for cross-account access.
Service-level isolation
Using kms:ViaService conditions in key policy statements restricts a CMK to a specific AWS service. An S3-scoped key cannot be used by a Lambda function calling Decrypt directly β even if that Lambda has an IAM policy permitting it.
Key Design Decisions
A single CMK per account means an RDS admin can decrypt S3 data if the same key is used for both. Separate CMKs per service (S3, RDS, Secrets Manager, EBS) mean each service's data is independently protected.
PatternUse key aliases to make this manageable: alias/prod/s3, alias/prod/rds, alias/prod/secrets. Tag each key with Service, Environment, and Owner for cost allocation and access review.
KMS supports automatic rotation for symmetric customer-managed keys. When rotation is enabled, KMS generates new key material on the configured schedule and keeps previous versions to decrypt data encrypted before rotation. The key ID and ARN do not change.
Update (2024+)KMS now supports configurable rotation periods β you can set any interval between 90 and 2,560 days, not just annual. This is significant for compliance frameworks that require rotation more frequently than once a year (e.g. PCI-DSS environments mandating 90-day rotation). Annual remains the default if you enable rotation without specifying a period. AWS-managed keys still rotate every year automatically and cannot have rotation disabled.
NoteRotation does not re-encrypt existing data. It only ensures new DEKs are wrapped with fresh key material. For data encrypted years ago, consider a periodic re-encrypt pass if your compliance policy requires it.
Key policy changes require administrative access and are harder to scope temporally. KMS grants allow you to give a specific principal limited permissions on a key β for example, kms:Decrypt only β and retire the grant when the access is no longer needed without modifying the key policy.
An EC2 AMI copy operation that reads an encrypted snapshot from another account uses a grant scoped to the operation's IAM role. When the AMI copy completes, the grant is retired. No permanent key policy modification was needed.
Without kms:ViaService conditions, a principal with kms:Decrypt permission can call the KMS API directly and decrypt any ciphertext created with that key β including data they should not access directly.
Restrict the Decrypt permission on service-specific keys to the service itself: "StringEquals": {"kms:ViaService": "s3.us-east-1.amazonaws.com"}. Direct API calls return AccessDeniedException, even from principals the key policy otherwise trusts.
Tradeoffs
| Decision | Benefit | Cost |
|---|---|---|
| One CMK per service per account | Service-level blast radius containment; independent audit trail per data domain | More keys to manage; higher monthly cost ($1/key/month for CMKs); Terraform complexity |
| Automatic key rotation enabled | Limits exposure window of any single key version; satisfies many compliance frameworks automatically | Does not re-encrypt existing data; rotation latency can briefly affect high-throughput services (check AWS docs for current behaviour) |
| kms:ViaService conditions on user statements | Prevents direct API decryption; enforces access through service control plane only | Increases key policy complexity; requires updating conditions when adding new regions or services |
| Separate key admin and key user roles | No single role can both control a key and use it; satisfies dual-control audit requirements | Requires extra IAM role per account; operational friction when a key policy change and data access are needed simultaneously |
Terraform Pattern
data "aws_caller_identity" "current" {}
resource "aws_kms_key" "s3" {
description = "S3 encryption key β ${var.environment}"
enable_key_rotation = true
deletion_window_in_days = 30
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EnableIAMPolicies"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
{
Sid = "KeyAdministration"
Effect = "Allow"
Principal = { AWS = var.key_admin_role_arn }
Action = [
"kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*",
"kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*",
"kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
]
Resource = "*"
},
{
Sid = "KeyUsage"
Effect = "Allow"
Principal = { AWS = var.s3_service_role_arn }
Action = ["kms:GenerateDataKey", "kms:Decrypt"]
Resource = "*"
Condition = {
StringEquals = {
"kms:ViaService" = "s3.${var.region}.amazonaws.com"
}
}
}
]
})
tags = {
Service = "s3"
Environment = var.environment
Owner = var.team_name
}
}
resource "aws_kms_alias" "s3" {
name = "alias/${var.environment}/s3"
target_key_id = aws_kms_key.s3.key_id
}
Closing Thought
Encryption enabled is not the same as encryption effective. The difference is key isolation. One CMK encrypting everything in an organisation is a padlock on a door where every employee has a master key β the lock exists, but it does not limit access.
A well-designed KMS hierarchy treats keys as access control boundaries, not just encryption mechanisms. Each CMK defines who can read which data β and the key policy makes that boundary explicit, auditable, and enforceable independently of IAM. That is the difference between compliance and security.
We look at how to design a multi-region Aurora deployment that survives a full regional failure with under 1 RPO β and the operational decisions that most teams get wrong when they plan for failover but not for failback.
Official AWS Reference
AWS KMS Developer Guide β Key policies in AWS KMS
β covers key policy format, required elements, cross-account delegation, and condition keys including kms:ViaService and kms:CallerAccount. The default key policy section is particularly useful when evaluating whether your root account statement is correctly scoped.
Comments