How to Fix AWS KMS AccessDenied 'kms:Decrypt' When Using Grants Instead of Key Policies
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Your IAM principal hit
AccessDeniedonkms:Decryptbecause KMS grants do not bypass a missing or restrictive key policy — the key policy must explicitly allow the principal or delegate to IAM first. - How to fix it: Add an explicit
kms:DecryptAllow in the key policy for the target principal, OR ensure the key policy contains the mandatory IAM delegation statement ("Principal": {"AWS": "arn:aws:iam::ACCOUNT_ID:root"}) so grants can take effect. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your key policy and get a corrected diff without leaking your ARNs.
The Incident (What Does the Error Mean?)
Raw error:
An error occurred (AccessDenied) when calling the Decrypt operation:
User: arn:aws:iam::123456789012:role/my-app-role is not authorized
to perform: kms:Decrypt on resource: arn:aws:kms:us-east-1:123456789012:key/mrk-abc123
This fires at runtime — your Lambda, EC2, or ECS task cannot decrypt an S3 object, RDS secret, or Secrets Manager value. The workload is dead until this is resolved. The confusion here is that engineers often create a KMS grant believing it alone is sufficient. It is not.
KMS access control uses a two-layer model:
- Key Policy — resource-based policy attached directly to the key. This is the gatekeeper.
- IAM Policy + Grants — identity-based controls. These only work if the key policy explicitly permits them to.
A grant is silently ignored if the key policy does not first delegate authority to IAM. This is the single most common cause of this exact error.
The Attack Vector / Blast Radius
This is not just an ops annoyance — the misconfiguration has a dangerous mirror image.
Scenario A — Over-restriction (your current problem): Legitimate workloads can't decrypt. Encrypted RDS snapshots are unrestorable. Secrets Manager rotations fail silently. On-call engineers start disabling encryption to restore service — trading a broken config for a real security hole.
Scenario B — The dangerous inverse: Engineers "fixing" this under pressure often swing to "kms:*" with "Principal": "*" in the key policy. Now any authenticated AWS principal in the account — or depending on the condition block, any AWS principal globally — can decrypt your data. A compromised IAM credential anywhere in the account becomes a full data exfiltration vector.
Blast radius of the original misconfiguration:
- All services using this CMK (Customer Managed Key) for envelope encryption are blocked: S3 SSE-KMS, EBS volumes, RDS, Secrets Manager, Parameter Store SecureString.
- If this key encrypts EBS root volumes, EC2 instances cannot start after a stop/start cycle.
- CloudTrail will log a flood of
kms:Decryptdenied events — a secondary alert storm.
How to Fix It
Root Cause Checklist (run this first)
# 1. Inspect the current key policy
aws kms get-key-policy \
--key-id arn:aws:kms:us-east-1:123456789012:key/mrk-abc123 \
--policy-name default \
--output text
# 2. List existing grants on the key
aws kms list-grants \
--key-id arn:aws:kms:us-east-1:123456789012:key/mrk-abc123
# 3. Simulate the access (replace ARN)
aws kms simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/my-app-role \
--action-names kms:Decrypt \
--resource-arns arn:aws:kms:us-east-1:123456789012:key/mrk-abc123
If the key policy has no "Principal": {"AWS": "arn:aws:iam::ACCOUNT_ID:root"} statement — that is your root cause. Grants cannot function without it.
Basic Fix — Add IAM Delegation + Explicit Decrypt Allow
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
- "AWS": "arn:aws:iam::123456789012:role/my-app-role"
+ "AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:*",
"Resource": "*"
},
+ {
+ "Sid": "AllowAppRoleDecrypt",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::123456789012:role/my-app-role"
+ },
+ "Action": [
+ "kms:Decrypt",
+ "kms:DescribeKey"
+ ],
+ "Resource": "*"
+ }
]
}
Critical: The
rootprincipal statement does not give root user access to decrypt — it tells KMS to honor IAM policies and grants attached to principals in this account. Without it, IAM is irrelevant for this key.
Enterprise Best Practice — Least Privilege with Condition Keys
Do not grant broad kms:Decrypt to a role without scoping it. Use kms:ViaService and kms:CallerAccount condition keys to restrict decryption to specific AWS services and your account only.
{
"Sid": "AllowAppRoleDecryptViaSecretsManager",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/my-app-role"
},
- "Action": "kms:*",
- "Resource": "*"
+ "Action": [
+ "kms:Decrypt",
+ "kms:DescribeKey"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "StringEquals": {
+ "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com",
+ "kms:CallerAccount": "123456789012"
+ }
+ }
}
This ensures the role can only decrypt when the call originates from Secrets Manager in your account — not from a rogue CLI session or a compromised Lambda invoking KMS directly.
If you genuinely need grants (e.g., for cross-account access or AWS service-linked roles), ensure the key policy root delegation exists first, then:
aws kms create-grant \
--key-id arn:aws:kms:us-east-1:123456789012:key/mrk-abc123 \
--grantee-principal arn:aws:iam::123456789012:role/my-app-role \
--operations Decrypt DescribeKey \
--name "app-role-decrypt-grant"
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
The goal: catch missing IAM delegation statements and overly permissive key policies before they hit production.
1. Checkov (Terraform)
# Catches KMS key policies missing key rotation and overly permissive principals
checkov -d ./terraform --check CKV_AWS_7,CKV2_AWS_64
CKV2_AWS_64 specifically flags KMS key policies that lack a proper key policy or allow Principal: *.
2. OPA / Conftest Policy
# policy/kms_key_policy.rego
package kms
deny[msg] {
stmt := input.Statement[_]
stmt.Effect == "Allow"
stmt.Action == "kms:*"
stmt.Principal == "*"
msg := "KMS key policy grants kms:* to wildcard principal — immediate data exfiltration risk"
}
deny[msg] {
stmts := [s | s := input.Statement[_]; s.Principal.AWS == "arn:aws:iam::*:root"]
count(stmts) == 0
msg := "KMS key policy missing IAM delegation (root principal) — grants will not function"
}
aws kms get-key-policy --key-id $KEY_ID --policy-name default --output text \
| conftest test - --policy policy/kms_key_policy.rego
3. AWS Config Rule
Enable the managed rule kms-cmk-not-scheduled-for-deletion and write a custom Config rule using kms:GetKeyPolicy to assert the IAM delegation statement is present on all CMKs tagged for production workloads.
4. Terraform — Enforce via aws_kms_key + aws_kms_key_policy
resource "aws_kms_key_policy" "app_key" {
key_id = aws_kms_key.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EnableIAMDelegation"
Effect = "Allow"
Principal = {
- AWS = "arn:aws:iam::${var.account_id}:role/${var.app_role_name}"
+ AWS = "arn:aws:iam::${var.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
+ Sid = "LeastPrivilegeDecrypt"
+ Effect = "Allow"
+ Principal = {
+ AWS = "arn:aws:iam::${var.account_id}:role/${var.app_role_name}"
+ }
+ Action = ["kms:Decrypt", "kms:DescribeKey"]
+ Resource = "*"
+ Condition = {
+ StringEquals = {
+ "kms:CallerAccount" = var.account_id
+ }
+ }
}
]
})
}
Pin this pattern in your internal Terraform module registry. Every team consuming KMS gets the delegation statement by default — no one ships a broken key policy again.