AWS KMS Key Policies: get this right or lose your data

How KMS key-policy evaluation works: cross-account access, condition keys, grants, key rotation, production patterns. With JSON policy examples and a production checklist.

· 15 min read · Đọc bản tiếng Việt

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:ViaService restricts the key to a specific service (blocks direct kms:Decrypt); kms:EncryptionContext is single-valued — do NOT use ForAllValues / ForAnyValue with it.
  • Envelope encryption is mandatory for data > 4KB: GenerateDataKey to obtain a DEK, encrypt locally, scrub the plaintext DEK from memory — avoids KMS throttling.
  • Grants are eventually consistent — use the GrantToken immediately to bypass propagation delay; separate Key Administrators (no Encrypt/Decrypt) from Key Users (no PutKeyPolicy) 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:

  1. The key policy is the primary authorization mechanism — if the key policy does not allow (directly or indirectly), no IAM policy can grant access.
  2. IAM policy works only when the key policy “delegates” — through a statement that allows the account root (arn:aws:iam::ACCOUNT:root).
  3. Each key has exactly one key policy with a 32 KB limit — you can’t attach multiple policies the way IAM does.
  4. 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

MechanismWhen to useCharacteristics
Key PolicyAlways present. For: static access, key admins, service integrations1 per key, 32KB limit, attached directly to the key
IAM PolicyManaging access at scale, ABAC/RBAC, multi-keyRequires root delegation in key policy, centralized management
GrantsTemporary/dynamic access, AWS service integrationsCreated 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:

  1. The security team wants to “harden” the key policy — only allow specific roles.
  2. They replace the entire key policy, dropping the Enable IAM User Permissions statement.
  3. The new key policy lists 2–3 specific roles.
  4. Everything works… until they need to add a new role.
  5. Nobody can PutKeyPolicy because the key policy doesn’t grant kms:PutKeyPolicy to anyone.
  6. 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:Decrypt on the source KMS key (Account A)
  • kms:Encrypt on 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:

  1. Account A shares the RDS snapshot with Account B.
  2. Account B copies the snapshot, specifying its own KMS key.
  3. 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:ViaService to 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?

  1. 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.

  2. Temporary access — when you don’t know ahead of time which principal will need access (e.g. a Lambda function created dynamically).

  3. Cross-account delegation — the grantee can live in a different account.

  4. 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 contains Department=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_KMS origin.
  • 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:

  1. Check the key policy: aws kms get-key-policy --key-id KEY_ID --policy-name default
  2. Confirm there’s a statement with "Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"} and "Action": "kms:*".
  3. If missing → add it via aws kms put-key-policy (you need kms:PutKeyPolicy from 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:

  1. Does the key policy list the external account/principal?
  2. Does the IAM policy in account B specify the correct key ARN? (Don’t use * for cross-account KMS.)
  3. Is any SCP denying KMS actions?
  4. Is the key in Enabled state? (aws kms describe-key)
  5. 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:

  1. 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
  1. Share the new copy (encrypted under the CMK) with Account B.
  2. 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:

  1. Does the replication configuration have SseKmsEncryptedObjects: Enabled?
  2. Does the replication configuration have ReplicaKmsKeyID?
  3. Does the replication role have kms:Decrypt on the source key?
  4. Does the replication role have kms:Encrypt on the destination key?
  5. 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:root with kms:*)
  • Key administrators and key users are separated (separation of duties)
  • No Principal: "*" without a condition (kms:CallerAccount or kms:ViaService)
  • Cross-account access (if any) is configured on both sides: key policy + IAM policy
  • kms:ViaService is used to restrict key usage to specific services
  • kms:GrantIsForAWSResource: true is set on CreateGrant permissions

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 ScheduleKeyDeletion except 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:Decrypt failures (may indicate unauthorized access attempts)
  • AWS Config rule cmk-backing-key-rotation-enabled active
  • Tag strategy for KMS keys (Environment, Team, Application, CostCenter)
  • Regular audit: aws kms list-grants to 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:CreateKey with kms:MultiRegion if data residency requires it
  • SCP denies kms:PutKeyPolicy for 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.