A team locked themselves out of their own encryption key. While tightening a KMS key policy to follow “least privilege,” they removed the statement granting the account root full permissions — a statement that looked redundant next to the more specific IAM-role statements they’d just added. Within an hour, every service using that key started failing with AccessDeniedException, including the IAM admins who’d made the change, because KMS key policies are the actual source of truth — IAM policies only grant permission if the key policy also allows it, and removing the root statement had removed the one universal escape hatch.
KMS access errors are almost always a mismatch between what the key policy says and what the caller’s IAM policy says — and both have to agree.
The Problem
Encrypt, decrypt, or grant operations fail with permission errors:
| Symptom | What It Means |
|---|---|
AccessDeniedException despite an IAM policy allowing kms:Decrypt |
The key policy doesn’t grant the same permission — both must agree |
| Cross-account decrypt fails | Key policy doesn’t include the external account, or the external account’s IAM policy doesn’t allow the action |
| Works for one IAM role, fails for another with an identical-looking policy | Key policy grants access to a specific role ARN, not a wildcard, and the failing role isn’t listed |
| Root account itself gets denied | Root permission statement was removed from the key policy, an easy self-lockout |
| Grant-based access (e.g., from an AWS service) stops working | Grant was retired or revoked, or the grantee principal changed |
KMS deliberately requires two independent “yes” votes — the key policy and the caller’s identity-based policy — and a denial from either one is enough to block the operation.
Why Does This Happen?
- Key policy doesn’t include the calling principal at all: Unlike most AWS resources, KMS key policies are the primary access control mechanism. An IAM policy granting
kms:Decrypt: *is not sufficient by itself — the key’s own policy must also explicitly allow that principal (directly, or by delegating to IAM via the"Enable IAM User Permissions"statement). - The default root/IAM delegation statement was removed: Every new key ships with a statement granting the account root full access, which effectively delegates control to IAM policies. Removing it during a “cleanup” means only statements explicitly listed in the key policy work — IAM policies alone are no longer sufficient.
- Cross-account access requires permission on both sides: The resource account’s key policy must explicitly grant the external account (or specific principal), AND the external account’s own IAM policy must allow the action — missing either one fails silently with the same generic
AccessDeniedException. - Condition keys scoping access more narrowly than expected: Conditions like
kms:ViaService,kms:EncryptionContext, orkms:CallerAccountcan restrict a grant to a specific service or context — a request that doesn’t match the condition (e.g., calling KMS directly instead of via S3) gets denied even though the principal is listed. - Grants revoked or expired: Temporary access granted via
CreateGrant(common for services like EBS or Lambda that need ephemeral key access) can be explicitly retired or can reference a principal that no longer exists (a deleted role), silently breaking access that used to work. - Using the wrong key ARN across regions or aliases: KMS keys are region-specific, and an alias (
alias/orders-key) can point to a different underlying key ID than expected after a key rotation event tied to alias repointing — the caller might have permission on the alias’s old target but not the new one.
The Fix
Step 1: Get the Exact Denial Reason From CloudTrail
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--query "Events[?contains(CloudTrailEvent, 'AccessDenied')]"
CloudTrail records show the exact error message, which distinguishes “denied by key policy” from “denied by IAM policy” — critical for knowing which side to fix.
Step 2: Retrieve and Read the Actual Key Policy
aws kms get-key-policy \
--key-id arn:aws:kms:us-east-1:111122223333:key/abc-123-def \
--policy-name default \
--output text
Confirm the delegation statement is present:
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "kms:*",
"Resource": "*"
}
If it was removed, restore it (or add a specific statement for the exact roles that need access):
aws kms put-key-policy \
--key-id arn:aws:kms:us-east-1:111122223333:key/abc-123-def \
--policy-name default \
--policy file://restored-key-policy.json
Step 3: Check the Calling Principal’s IAM Policy
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/orders-service-role \
--action-names kms:Decrypt \
--resource-arns arn:aws:kms:us-east-1:111122223333:key/abc-123-def
simulate-principal-policy evaluates the IAM side only — combine this with Step 2 to know whether the block is on the key policy side, the IAM side, or both.
Step 4: Verify Cross-Account Access on Both Sides
On the key owner’s account, confirm the external account/principal is listed:
aws kms get-key-policy \
--key-id arn:aws:kms:us-east-1:111122223333:key/abc-123-def \
--policy-name default \
--query "Policy" | grep -A 3 "444455556666"
On the calling account, confirm the IAM policy allows the action against the specific key ARN (not a wildcard resource that happens to look permissive but doesn’t match cross-account KMS ARNs):
aws iam get-role-policy \
--role-name external-reader-role \
--policy-name kms-access
Step 5: Check for Condition Keys Restricting the Request Context
aws kms get-key-policy \
--key-id arn:aws:kms:us-east-1:111122223333:key/abc-123-def \
--policy-name default \
--query "Policy" | grep -A 5 "Condition"
If kms:ViaService is present, confirm the request is actually coming through that service (e.g., s3.us-east-1.amazonaws.com) and not a direct KMS API call, which would fail the condition.
Step 6: Audit Active Grants
aws kms list-grants \
--key-id arn:aws:kms:us-east-1:111122223333:key/abc-123-def \
--query "Grants[*].{GranteePrincipal:GranteePrincipal,Operations:Operations,GrantId:GrantId}" \
--output table
If the expected grantee (an EBS volume, a Lambda execution role) isn’t listed, the grant may have been revoked or never created — recreate it via the service’s own provisioning flow rather than manually, since most AWS service integrations manage their own grants automatically.
Step 7: Confirm Alias Resolves to the Expected Key
aws kms list-aliases \
--query "Aliases[?AliasName=='alias/orders-key'].TargetKeyId" \
--output text
Compare this to the key ID your application or IAM policy actually references — a repointed alias after a manual key rotation is a common source of “it worked yesterday” reports.
Is This Safe?
The get, list, and simulate-principal-policy calls are read-only. Restoring a key policy statement is safe and reversible — keep a copy of the current policy before overwriting it with put-key-policy, since this call replaces the entire policy document, not just the statement you’re adding. If a key policy lockout has already happened and the root delegation statement is gone, only a principal explicitly listed in the remaining key policy (or the account root itself, for the API call specifically) can fix it — plan key policy changes carefully and never remove the root delegation statement without a specific replacement already in place.
Key Takeaway
KMS is one of the few AWS services where the resource policy is not optional — both the key policy and the caller’s IAM policy must independently allow an action, and a denial from either side produces the same generic error. Before debugging IAM roles, pull the actual key policy and confirm the calling principal (or the IAM delegation statement) is present in it. Never remove the default root delegation statement unless you’ve already added specific replacement statements for every principal that needs access — it’s the easiest self-inflicted lockout in the service.
Have questions or ran into a different KMS issue? Connect with me on LinkedIn or X.