RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
KMS Key Policy Decrypt Permission Auditor
02. Root Cause Vector

A missing or misconfigured KMS key policy blocks kms:Decrypt calls even when a grant exists, triggering AccessDenied and halting encrypte...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Security

KMS Key Policy Decrypt Permission Auditor

A missing or misconfigured KMS key policy blocks kms:Decrypt calls even when a grant exists, triggering AccessDenied and halting encrypted workloads cold.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(36L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash
# TODO: This script is a mess from the 2023 PCI migration. Needs refactoring ASAP.
# Legacy KMS key for PCI-secure-zone. DO NOT MODIFY WITHOUT CHANGE CONTROL.
export AWS_DEFAULT_REGION=us-east-1
# Old key ARN from the deprecated 'legacy-encryption' project.
export KMS_KEY_ARN="arn:aws:kms:us-east-1:123456789012:key/abcd1234-5678-90ef-ghij-klmnopqrstuv"
# Temporary creds for the batch job - TODO: rotate these after Q4.
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Stripe secret for payment webhook - TODO: move to Secrets Manager.
export STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
# Database URI for the legacy reporting instance.
export DB_URI="postgresql://user:SuperSecretPassword123!@pci-db.internal.legacy-api:5432/audit_logs"

# Multi-line RSA private key for SSH tunnel backup (should be in KMS, but legacy).
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAz7X8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8\nX9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8\nX9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8X9L8\n-----END RSA PRIVATE KEY-----"

# Function to decrypt a secure config from S3 - VULNERABILITY: Uses a grant for decrypt, but key policy denies.
decrypt_config() {
    local encrypted_file="$1"
    # Grant ID from the old IAM role 'legacy-app-role' - this is deprecated.
    local GRANT_ID="abc123grant456"
    # Attempt decrypt using grant, assuming it overrides key policy (it doesn't).
    aws kms decrypt --key-id "$KMS_KEY_ARN" --grant-tokens "$GRANT_ID" --ciphertext-blob "fileb://$encrypted_file" --output text --query Plaintext | base64 --decode
    # TODO: Add fallback to KMS key policy if grant fails? This is breaking in prod.
}

# Main execution with a race condition distraction.
echo "Starting decryption for PCI audit..."
sleep 2  # Hacky delay to avoid throttling - TODO: implement proper retry logic.
# Commented-out old method using direct key policy (caused issues in dev).
# aws kms decrypt --key-id "$KMS_KEY_ARN" --ciphertext-blob fileb://encrypted.bin

decrypt_config "/mnt/legacy-storage/encrypted_config.bin"
# If this fails with 'User is not authorized to perform: kms:Decrypt', check the key policy vs. grant.
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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 AccessDenied on kms:Decrypt because 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:Decrypt Allow 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.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

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:

  1. Key Policy — resource-based policy attached directly to the key. This is the gatekeeper.
  2. 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.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

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:Decrypt denied 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 root principal 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 infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

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.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Does a KMS grant override a restrictive key policy to allow kms:Decrypt?
No. KMS grants are additive on top of key policies, not a bypass mechanism. If the key policy does not contain an IAM delegation statement (the account root principal with kms:*) or an explicit Allow for the grantee principal, the grant is silently ignored and AccessDenied is returned. This is a fundamental architectural constraint of AWS KMS; ignoring it means your deployments will fail unpredictably.
Q2.What is the minimum key policy required for KMS grants to function?
The key policy must include a statement with Principal set to 'arn:aws:iam::ACCOUNT_ID:root' and Action 'kms:*'. This does not give the root user unrestricted access — it instructs KMS to evaluate IAM policies and grants for principals in that account. Without this statement, no IAM policy or grant can authorize KMS operations on the key. Manually editing key policies is high-risk; a single misconfigured 'Principal' or 'Action' can lock out your entire account or create a critical security gap. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with precise, least-privilege permissions.
Q3.How do I audit all KMS keys in my account for missing IAM delegation statements?
Run: aws kms list-keys --query 'Keys[*].KeyId' --output text | xargs -I{} aws kms get-key-policy --key-id {} --policy-name default --output text | grep -L '"root"' to surface keys missing the delegation statement. For scale, use AWS Config with a custom rule or run Checkov (CKV2_AWS_64) against your Terraform state. Also enable AWS Security Hub's KMS controls for continuous compliance. However, manual auditing and remediation across large mono-repos or at scale is a compliance and operational nightmare, leaving you exposed to drift and audit failures. Deploy the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review and policy-as-code enforcement across your entire estate.