KMS is a service almost everyone uses, yet few people really understand its authorization model in depth. Unlike every other AWS service, KMS has its own dual-authorization system — key policy plus IAM policy plus grants — and if you misconfigure it, you can lose all access to a key with no way to recover other than calling AWS Support. This post shares everything I’ve learned from running KMS in production: evaluation logic, cross-account patterns, condition keys, and the production patterns I actually use — every one with JSON policy examples you can copy and adapt.
TL;DR
- KMS has a unique dual-authorization model: key policy (resource-based) + IAM policy + grants — get it wrong and you can lose all access to the key; only AWS Support can recover it.
- The default key policy with
Principal: arn:aws:iam::ACCOUNT:root+kms:*is a delegation statement (not a root grant); remove it and the key is “locked out”.- Same-account: one of the three mechanisms allowing is enough; cross-account: BOTH sides must allow (the owner’s key policy AND the consumer’s IAM policy), with the correct key ARN.
- AWS managed keys (
aws/s3,aws/ebs) CANNOT be shared cross-account — production must use customer-managed keys.kms:ViaServicerestricts the key to a specific service (blocks directkms:Decrypt);kms:EncryptionContextis single-valued — do NOT useForAllValues/ForAnyValuewith it.- Envelope encryption is mandatory for data > 4KB:
GenerateDataKeyto obtain a DEK, encrypt locally, scrub the plaintext DEK from memory — avoids KMS throttling.- Grants are eventually consistent — use the
GrantTokenimmediately to bypass propagation delay; separate Key Administrators (noEncrypt/Decrypt) from Key Users (noPutKeyPolicy) for separation of duties.
What a KMS key policy is and why it’s special
Core difference from IAM policy
With most AWS services (S3, EC2, Lambda…) an Allow IAM policy is enough to access the resource. KMS doesn’t work like that.
Every KMS key must have a key policy — this is the single resource-based policy attached directly to the key. The key differences:
- The key policy is the primary authorization mechanism — if the key policy does not allow (directly or indirectly), no IAM policy can grant access.
- IAM policy works only when the key policy “delegates” — through a statement that allows the account root (
arn:aws:iam::ACCOUNT:root). - Each key has exactly one key policy with a 32 KB limit — you can’t attach multiple policies the way IAM does.
- The key policy can stand alone — no IAM policy needed if the key policy already grants the principal directly.
In other words: with an S3 bucket, you can use an IAM policy OR a bucket policy. With KMS, the key policy is always evaluated, and IAM policy is only an “extension” when the key policy allows it.
Why design it this way?
KMS manages encryption keys — and if you lose control of them, you lose the data with them. AWS designed dual-authorization to:
- Prevent privilege escalation: an IAM admin can’t grant themselves the right to use a KMS key if the key policy doesn’t delegate.
- Separate duties: the key administrator (who manages the key) and the key user (who uses it to encrypt/decrypt) can be different people.
- Control blast radius: removing an IAM policy doesn’t affect anyone the key policy grants directly.
Evaluation logic: key policy + IAM policy + grants
Authorization flowchart
When a principal calls a KMS API (e.g. kms:Decrypt), AWS evaluates with the following logic:
┌─────────────────────────────────────────────────────────────┐
│ KMS Authorization Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Request arrives ──→ Any Explicit Deny? │
│ (Key policy, IAM, SCP, VPC endpoint) │
│ │ │
│ YES ─┘──→ ❌ DENY │
│ NO ─┐ │
│ ▼ │
│ Key policy grants this │
│ principal directly? │
│ │ │
│ YES ─┘──→ ✅ ALLOW │
│ NO ─┐ │
│ ▼ │
│ Key policy delegates to │
│ the account root? │
│ │ │
│ NO ─┘──→ ❌ DENY (IAM is useless) │
│ YES ─┐ │
│ ▼ │
│ IAM policy OR Grant │
│ allows the action? │
│ │ │
│ YES ─┘──→ ✅ ALLOW │
│ NO ─┘──→ ❌ DENY │
│ │
└─────────────────────────────────────────────────────────────┘
The three grant mechanisms
| Mechanism | When to use | Characteristics |
|---|---|---|
| Key Policy | Always present. For: static access, key admins, service integrations | 1 per key, 32KB limit, attached directly to the key |
| IAM Policy | Managing access at scale, ABAC/RBAC, multi-key | Requires root delegation in key policy, centralized management |
| Grants | Temporary/dynamic access, AWS service integrations | Created via API, eventually consistent, doesn’t count against policy size |
Same-account vs cross-account
Same-account — only ONE of the three mechanisms needs to allow (assuming the key policy delegates to root):
Key policy grants directly → ALLOW
OR
Key policy delegates root + IAM policy allows → ALLOW
OR
Key policy delegates root + Grant allows → ALLOW
Cross-account — BOTH sides required:
Key policy (account A) allows the external principal
AND
IAM policy (account B) allows the action on the key ARN
This is why cross-account KMS access is much more complicated than cross-account S3 or cross-account AssumeRole.
The default key policy — a trap everyone hits
What the default key policy looks like
When you create a KMS key through the Console or CLI without specifying a custom policy, AWS creates this default key policy:
{
"Version": "2012-10-17",
"Id": "key-default-1",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "kms:*",
"Resource": "*"
}
]
}
This statement does not grant the root user access. It’s a delegation statement — it tells KMS: “allow IAM policies in this account to grant access to this key”.
The trap: deleting the delegation statement
Here’s a scenario I’ve seen happen in production:
- The security team wants to “harden” the key policy — only allow specific roles.
- They replace the entire key policy, dropping the
Enable IAM User Permissionsstatement. - The new key policy lists 2–3 specific roles.
- Everything works… until they need to add a new role.
- Nobody can
PutKeyPolicybecause the key policy doesn’t grantkms:PutKeyPolicyto anyone. - End state: call AWS Support to recover.
Best practice: always keep the delegation statement + separate key admin from key user
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnableIAMPolicies",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "KeyAdministrators",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/KMSAdminRole"
},
"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": "*"
},
{
"Sid": "KeyUsers",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/AppRole"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
Note: Key Administrators do not have Encrypt/Decrypt — they manage the key but don’t use it. Key Users do not have PutKeyPolicy/ScheduleKeyDeletion — they use the key but don’t manage it. This is basic separation of duties.
Gotcha with Organizations
When using AWS Organizations, OrganizationAccountAccessRole in member accounts has AdministratorAccess. If the key policy has the delegation statement (root), this role can use any KMS key in the account. Mitigation:
- Use SCPs to restrict KMS actions at the Organization level.
- Or drop the delegation statement and use explicit principals (but be careful — see the gotcha above).
Cross-account access patterns
Cross-account KMS access is one of the most complex things in AWS. I’ll walk through the three most common patterns.
Pattern 1: Sharing an encrypted EBS snapshot
Scenario: Account A (111122223333) has an encrypted EBS snapshot and wants to share it with Account B (444455556666).
Step 1 — Key policy in Account A (the key owner):
{
"Sid": "AllowExternalAccountUse",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:root"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "*"
}
Step 2 — IAM policy in Account B (the consumer):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "arn:aws:kms:us-east-1:111122223333:key/key-id"
}
]
}
Step 3 — Account B copies the snapshot to its own key:
aws ec2 copy-snapshot \
--source-region us-east-1 \
--source-snapshot-id snap-0123456789abcdef0 \
--encrypted \
--kms-key-id arn:aws:kms:us-east-1:444455556666:key/account-b-key-id \
--description "Re-encrypted copy from Account A"
Step 3 is mandatory — you should not depend on another account’s key long-term. Copy and re-encrypt under your own key to keep full control.
Why does kms:CreateGrant matter? Because when the EC2 service needs to decrypt snapshot data, it creates a grant on the KMS key instead of calling Decrypt directly. Without CreateGrant, the snapshot copy fails.
Pattern 2: Cross-account S3 with SSE-KMS
Scenario: S3 cross-region replication from Account A to Account B, with both buckets using SSE-KMS.
The replication role in Account A needs:
kms:Decrypton the source KMS key (Account A)kms:Encrypton the destination KMS key (Account B)
The destination key’s policy (Account B) has to allow the replication role from Account A:
{
"Sid": "AllowS3ReplicationFromAccountA",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/S3ReplicationRole"
},
"Action": [
"kms:Encrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.eu-west-1.amazonaws.com"
}
}
}
The replication configuration must specify the destination KMS key:
{
"Rules": [
{
"Status": "Enabled",
"Destination": {
"Bucket": "arn:aws:s3:::destination-bucket",
"EncryptionConfiguration": {
"ReplicaKmsKeyID": "arn:aws:kms:eu-west-1:444455556666:key/dest-key-id"
}
},
"SourceSelectionCriteria": {
"SseKmsEncryptedObjects": {
"Status": "Enabled"
}
}
}
]
}
Pattern 3: Cross-account RDS snapshot
Same flow as EBS snapshot:
- Account A shares the RDS snapshot with Account B.
- Account B copies the snapshot, specifying its own KMS key.
- Restore from the re-encrypted copy.
Important note: AWS managed keys (aws/rds, aws/ebs, aws/s3) CANNOT be shared cross-account. You MUST use customer-managed keys for any cross-account scenario. This is why I always recommend CMKs over AWS managed keys in production.
Important condition keys
KMS has a powerful set of condition keys that most people underuse. Here are the three I use most often in production.
kms:ViaService — restrict the key to a specific service
This condition ensures the key can only be used when the request comes from a specific AWS service — no one can call the KMS API directly.
{
"Sid": "OnlyViaS3AndEC2",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/AppRole"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*",
"kms:CreateGrant",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": [
"s3.us-east-1.amazonaws.com",
"ec2.us-east-1.amazonaws.com"
]
}
}
}
Format: SERVICE.REGION.amazonaws.com
Real-world use case: You have a KMS key used for S3 encryption. You want AppRole to read/write S3 objects (S3 will call KMS on AppRole’s behalf), but you don’t want AppRole calling kms:Decrypt directly to decrypt the data key and then decrypt data offline. kms:ViaService is exactly the lever for that.
⚠️ Gotcha: With kms:ViaService set, a user can’t test the key via CLI (aws kms encrypt --key-id ...) — the request gets denied because it isn’t going through any service.
kms:CallerAccount — restrict by account
Instead of listing every principal in the key policy (which can exceed the 32KB limit), use Principal: "*" plus kms:CallerAccount:
{
"Sid": "AllowAllPrincipalsInAccount",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:CallerAccount": "111122223333"
}
}
}
When to use:
- An account with many roles/users needing key access (dozens to hundreds).
- You want to manage access through IAM policies instead of updating the key policy every time a role is added.
- Combine with
kms:ViaServiceto bound by both account and service.
⚠️ DANGEROUS if you use Principal: "*" WITHOUT a condition:
{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "kms:*",
"Resource": "*"
}
That policy allows every AWS account in the world to access your key. Always pair a wildcard principal with a condition.
kms:EncryptionContext — context-based authorization
Encryption context is a key-value pair you pass when encrypting/decrypting. KMS logs it in CloudTrail (in plaintext) and you can use it as a condition in policies.
{
"Sid": "OnlyDecryptProductionData",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/ProdAppRole"
},
"Action": "kms:Decrypt",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:Environment": "Production",
"kms:EncryptionContext:AppName": "PaymentService"
}
}
}
Real-world use case: You share a single KMS key across multiple microservices. Each service encrypts data with an encryption context containing its service name. The policy above ensures ProdAppRole can only decrypt data that PaymentService encrypted in the Production environment — even though they share the same key, it can’t decrypt any other service’s data.
⚠️ Important: kms:EncryptionContext:context-key is a single-valued condition key. NEVER use ForAllValues or ForAnyValue with it — that produces an overly permissive policy.
Advanced pattern — restrict to specific context keys only:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/AppRole"
},
"Action": "kms:GenerateDataKey",
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:Department": "Finance"
},
"ForAllValues:StringEquals": {
"kms:EncryptionContextKeys": ["Department"]
}
}
}
This policy requires: the encryption context must contain a Department key with value Finance, and may not contain any other key. Note ForAllValues here is paired with kms:EncryptionContextKeys (plural, multi-valued) — different from kms:EncryptionContext:key (single-valued).
Grants — when to use them, and patterns with AWS services
What is a grant?
A grant is the third mechanism for authorizing access to a KMS key, alongside key policy and IAM policy. Key differences:
- Created via API (
CreateGrant) — not a JSON policy document. - Eventually consistent — a newly created grant might not be active immediately.
- Retirable/revocable — suits temporary access.
- Doesn’t count against key-policy size — solves the 32KB limit.
When to use grants?
-
AWS service integrations — EBS, RDS, Redshift automatically create grants when using your CMK. When you launch an EC2 instance with an encrypted EBS volume, the EC2 service creates a grant on the KMS key to decrypt the data key.
-
Temporary access — when you don’t know ahead of time which principal will need access (e.g. a Lambda function created dynamically).
-
Cross-account delegation — the grantee can live in a different account.
-
Avoid key-policy bloat — when hundreds of principals need dynamic access.
Grant example
aws kms create-grant \
--key-id arn:aws:kms:us-east-1:111122223333:key/key-id \
--grantee-principal arn:aws:iam::444455556666:role/CrossAccountRole \
--operations Decrypt GenerateDataKey \
--constraints '{"EncryptionContextSubset":{"Department":"Finance"}}' \
--retiring-principal arn:aws:iam::111122223333:role/SecurityAdmin
What each piece does:
--grantee-principal: who gets the access.--operations: only Decrypt and GenerateDataKey — not all actions.--constraints: only effective when the encryption context containsDepartment=Finance.--retiring-principal: who can retire (delete) this grant.
Pattern: key policy allowing AWS services to create grants
{
"Sid": "AllowGrantsForAWSServices",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/AppRole"
},
"Action": "kms:CreateGrant",
"Resource": "*",
"Condition": {
"Bool": {
"kms:GrantIsForAWSResource": true
}
}
}
kms:GrantIsForAWSResource: true ensures only AWS services (EBS, RDS, etc.) can create grants through this role — users cannot create grants directly themselves.
Gotcha: eventual consistency
Grants are eventually consistent. If you create a grant and immediately use the key, you may hit AccessDeniedException. The workaround:
import boto3
kms = boto3.client('kms')
# Create the grant and capture the grant token
response = kms.create_grant(
KeyId='arn:aws:kms:us-east-1:111122223333:key/key-id',
GranteePrincipal='arn:aws:iam::111122223333:role/TempRole',
Operations=['Decrypt']
)
grant_token = response['GrantToken']
# Use the grant token immediately — no need to wait for propagation
response = kms.decrypt(
CiphertextBlob=encrypted_data,
GrantTokens=[grant_token]
)
GrantToken lets you use the grant immediately without waiting for eventual consistency.
Key rotation strategies
KMS supports three rotation methods, each suited to a different use case.
Automatic rotation
The simplest method, recommended for most cases.
Characteristics:
- Only supports symmetric encryption keys with
AWS_KMSorigin. - Configurable period: 90–2560 days (default 365).
- New key material is generated; the old material is retained to decrypt old data.
- Key ID does not change — fully transparent to applications.
- AWS managed keys (
aws/s3,aws/ebs): always rotate every 365 days, not configurable.
# Enable automatic rotation with a 180-day period
aws kms enable-key-rotation \
--key-id 1234abcd-12ab-34cd-56ef-1234567890ab \
--rotation-period-in-days 180
# Check status
aws kms get-key-rotation-status \
--key-id 1234abcd-12ab-34cd-56ef-1234567890ab
Multi-region keys: rotation is a shared property — enable it on the primary key, and every replica rotates at the same time with the same key material.
On-demand rotation (introduced in 2024)
For when you need to rotate immediately — no waiting for the schedule.
Use cases:
- You suspect the key material was compromised.
- A compliance audit asks for proof of rotation capability.
- Testing the automation pipeline.
aws kms rotate-key-on-demand \
--key-id 1234abcd-12ab-34cd-56ef-1234567890ab
Note: on-demand rotation does not change the automatic rotation schedule. If automatic rotation is set to every 365 days and you trigger on-demand on day 100, the next automatic rotation still lands on day 365.
Manual rotation (alias rotation)
For keys that don’t support automatic rotation: asymmetric keys, HMAC keys, custom key store keys.
Pattern:
# 1. Create a new key
aws kms create-key --description "App encryption key v2"
# Output: key-id = NEW_KEY_ID
# 2. Point the alias at the new key
aws kms update-alias \
--alias-name alias/my-app-key \
--target-key-id NEW_KEY_ID
# 3. The old key is RETAINED (enabled) to decrypt old data
# Apps using the alias → automatically use the new key for encryption
# Decrypting old data → KMS picks the right key from the ciphertext metadata
Why use an alias? The application references the alias (alias/my-app-key) rather than the key ID. When rotating, you only update the alias — no changes to application code or config.
SCP to enforce rotation policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyShortRotationPeriod",
"Effect": "Deny",
"Action": "kms:EnableKeyRotation",
"Resource": "*",
"Condition": {
"NumericGreaterThan": {
"kms:RotationPeriodInDays": "365"
}
}
}
]
}
That SCP prevents anyone from setting a rotation period longer than 365 days — ensuring compliance requirements are met.
Production patterns: envelope encryption and multi-region keys
Envelope encryption — the core pattern
Every AWS service integration with KMS uses envelope encryption. Understanding this pattern is understanding how KMS actually works in practice.
┌──────────────────────────────────────────────────────────────┐
│ ENVELOPE ENCRYPTION │
├──────────────────────────────────────────────────────────────┤
│ │
│ Encryption: │
│ 1. App calls GenerateDataKey(CMK_ID) │
│ 2. KMS returns: │
│ • Plaintext data key (DEK) — used to encrypt data │
│ • Encrypted data key — stored alongside ciphertext │
│ 3. App encrypts data with plaintext DEK (AES-256 locally) │
│ 4. App SCRUBS plaintext DEK from memory │
│ 5. App stores: encrypted data + encrypted DEK │
│ │
│ Decryption: │
│ 1. App sends encrypted DEK to KMS Decrypt() │
│ 2. KMS returns the plaintext DEK │
│ 3. App decrypts data with the plaintext DEK │
│ 4. App SCRUBS plaintext DEK from memory │
│ │
└──────────────────────────────────────────────────────────────┘
Why not encrypt directly with KMS?
- KMS caps plaintext at 4 KB for direct encryption.
- Every encrypt/decrypt requires a KMS API call → latency + cost + throttling (RPS quota).
- Envelope encryption: 1 API call to get a data key → encrypt unlimited data locally → fast, cheap, no throttling.
Key policy for S3 SSE-KMS (production-ready)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnableIAMPolicies",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:root"},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowS3ServiceUse",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:role/S3AccessRole"},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.us-east-1.amazonaws.com",
"kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::my-secure-bucket/*"
}
}
}
]
}
The kms:EncryptionContext:aws:s3:arn condition restricts the key to objects in a specific bucket only — even if the role has access to several buckets, this key only works with my-secure-bucket.
Key policy for EBS encryption
{
"Sid": "AllowEBSEncryption",
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:CreateGrant",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:CallerAccount": "111122223333",
"kms:ViaService": "ec2.us-east-1.amazonaws.com"
}
}
}
This pattern uses Principal: "*" + kms:CallerAccount + kms:ViaService — allowing any principal in the account to use the key, but ONLY through the EC2 service. No one can call KMS directly.
Multi-region keys
Multi-region keys let you replicate key material to multiple regions — same key ID (with mrk- prefix), same key material, but each replica has its own key policy and grants.
Primary Region (us-east-1) Replica Region (eu-west-1)
┌─────────────────────┐ ┌─────────────────────┐
│ Primary MRK │ │ Replica MRK │
│ mrk-1234abcd... │───────────▶│ mrk-1234abcd... │
│ │ same key │ │
│ Encrypts data │ material │ Decrypts the same │
│ │ │ ciphertext │
└─────────────────────┘ └─────────────────────┘
Use cases:
- DR/failover: decrypt data in a backup region without cross-region KMS calls.
- Global applications: DynamoDB Global Tables, S3 cross-region replication.
- Client-side encryption: encrypt in region A, decrypt in region B.
Create a multi-region key:
# Create the primary key
aws kms create-key \
--multi-region \
--description "Global encryption key for payment data"
# Replicate to another region
aws kms replicate-key \
--key-id mrk-1234abcd12ab34cd56ef1234567890ab \
--replica-region eu-west-1 \
--policy file://replica-key-policy.json
Condition keys for multi-region:
{
"Effect": "Deny",
"Action": "kms:CreateKey",
"Resource": "*",
"Condition": {
"Bool": {
"kms:MultiRegion": true
}
}
}
That SCP prevents creating multi-region keys — useful when organization policy requires data not to be replicated outside specific regions.
Important notes:
- Each replica has its own key policy — you have to configure the policy for every replica.
- Rotation is a shared property — enable on the primary, it propagates to every replica.
- Deleting a replica doesn’t affect the primary or other replicas.
- Promote a replica to primary: useful when the primary region experiences an outage.
Troubleshooting: common errors and fixes
Error 1: AccessDeniedException on decrypt — IAM policy says Allow
Symptoms:
An error occurred (AccessDeniedException) when calling the Decrypt operation:
User: arn:aws:iam::111122223333:role/AppRole is not authorized to perform: kms:Decrypt
on resource: arn:aws:kms:us-east-1:111122223333:key/key-id
Most common cause: key policy is missing the delegation statement (root account).
Fix:
- Check the key policy:
aws kms get-key-policy --key-id KEY_ID --policy-name default - Confirm there’s a statement with
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"}and"Action": "kms:*". - If missing → add it via
aws kms put-key-policy(you needkms:PutKeyPolicyfrom the current key policy).
Error 2: cannot decrypt — key policy has a ViaService condition
Symptoms: decrypt works when going through S3 GetObject, but fails when calling aws kms decrypt directly.
Cause: the key policy has a kms:ViaService condition — access is restricted to specific services.
Fix: this is by design, not a bug. If you need direct decrypt access (e.g. for testing), add a separate statement without kms:ViaService for an admin role:
{
"Sid": "AdminDirectAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/AdminRole"
},
"Action": ["kms:Decrypt", "kms:Encrypt"],
"Resource": "*"
}
Error 3: cross-account access denied — both sides configured
Debug checklist:
- Does the key policy list the external account/principal?
- Does the IAM policy in account B specify the correct key ARN? (Don’t use
*for cross-account KMS.) - Is any SCP denying KMS actions?
- Is the key in
Enabledstate? (aws kms describe-key) - Is the region correct? KMS keys are regional.
Error 4: InvalidGrantTokenException
Symptoms: using a grant token but it’s rejected.
Cause: grant tokens have a short TTL (usually 5 minutes). Caching one too long means it expires.
Fix: create a fresh grant, or wait for the grant to propagate (typically < 5 minutes) and use it without the token.
Error 5: KMSInvalidStateException — key disabled or pending deletion
Symptoms:
An error occurred (KMSInvalidStateException) when calling the Encrypt operation:
arn:aws:kms:us-east-1:111122223333:key/key-id is pending deletion.
Fix:
# If pending deletion — cancel the deletion
aws kms cancel-key-deletion --key-id KEY_ID
# Then re-enable the key
aws kms enable-key --key-id KEY_ID
Prevention: use an SCP to block ScheduleKeyDeletion except for a break-glass role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyKeyDeletion",
"Effect": "Deny",
"Action": "kms:ScheduleKeyDeletion",
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/BreakGlassRole"
}
}
}
]
}
Error 6: encrypted snapshot share fails — AWS managed key
Symptoms: ModifySnapshotAttribute succeeds but Account B can’t copy the snapshot.
Cause: snapshot is encrypted with aws/ebs (AWS managed key) — this key can’t be shared cross-account.
Fix:
- In Account A, copy the snapshot to a CMK (customer-managed key):
aws ec2 copy-snapshot \
--source-region us-east-1 \
--source-snapshot-id snap-original \
--encrypted \
--kms-key-id arn:aws:kms:us-east-1:111122223333:key/cmk-id
- Share the new copy (encrypted under the CMK) with Account B.
- Update the CMK’s key policy to allow Account B.
Error 7: S3 replication fails with KMS-encrypted objects
Symptoms: replication status FAILED for KMS-encrypted objects, while unencrypted objects replicate fine.
Checklist:
- Does the replication configuration have
SseKmsEncryptedObjects: Enabled? - Does the replication configuration have
ReplicaKmsKeyID? - Does the replication role have
kms:Decrypton the source key? - Does the replication role have
kms:Encrypton the destination key? - Does the destination key policy allow the replication role?
Production KMS setup checklist
Before going live, make sure every item below is checked:
Key policy & access control
- Key policy has the delegation statement (
arn:aws:iam::ACCOUNT:rootwithkms:*) - Key administrators and key users are separated (separation of duties)
- No
Principal: "*"without a condition (kms:CallerAccountorkms:ViaService) - Cross-account access (if any) is configured on both sides: key policy + IAM policy
-
kms:ViaServiceis used to restrict key usage to specific services -
kms:GrantIsForAWSResource: trueis set onCreateGrantpermissions
Key rotation & lifecycle
- Automatic rotation enabled with a period that meets compliance (90–365 days)
- Use customer-managed keys (not AWS managed keys) for cross-account scenarios
- Application code uses key aliases (no hard-coded key IDs)
- SCP blocks
ScheduleKeyDeletionexcept for the break-glass role - Key deletion waiting period set to the maximum (30 days)
Encryption patterns
- Envelope encryption for data > 4KB
- Encryption context used for audit trail and fine-grained authorization
- S3 bucket policy enforces SSE-KMS (
s3:x-amz-server-side-encryption: aws:kms) - EBS default encryption enabled at the account level with a CMK
- Multi-region keys for DR scenarios (where needed)
Monitoring & compliance
- CloudTrail logging enabled for every KMS API call
- CloudWatch alarm for
KMS:Decryptfailures (may indicate unauthorized access attempts) - AWS Config rule
cmk-backing-key-rotation-enabledactive - Tag strategy for KMS keys (Environment, Team, Application, CostCenter)
- Regular audit:
aws kms list-grantsto review grants that are no longer needed
Organization-level controls
- SCP denies
kms:DisableKeyRotation(enforce rotation) - SCP denies
kms:ScheduleKeyDeletion(protect against accidental deletion) - SCP restricts
kms:CreateKeywithkms:MultiRegionif data residency requires it - SCP denies
kms:PutKeyPolicyfor non-admin roles
KMS key policies are one of those things you “set up once, forget about until it breaks” — and when it breaks, it’s usually right when you most need access to your data. Hopefully this post helps you understand the mechanics well enough to avoid the common mistakes and build a solid encryption strategy for production workloads.