How to Fix AWS KMS Access Denied: Missing Root IAM Delegation in Key Policy
Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: The KMS key policy omits a statement granting
kms:*toarn:aws:iam::ACCOUNT_ID:root, so AWS IAM cannot delegate key permissions to any IAM principal — every API call returnsAccessDeniedException. - How to fix it: Add the mandatory root delegation statement to the key policy, then attach scoped IAM policies to roles/users for actual usage permissions.
- Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your broken key policy and get a corrected version without sending your ARNs to a third-party server.
The Incident (What does the error mean?)
Raw error from CloudTrail / SDK:
{
"errorCode": "AccessDeniedException",
"errorMessage": "User: arn:aws:iam::123456789012:role/MyAppRole is not authorized
to perform: kms:Decrypt on resource:
arn:aws:kms:us-east-1:123456789012:key/mrk-abc123"
}
This fires even when the IAM role has an inline kms:Decrypt policy attached. KMS key policies are not optional addendums — they are the primary authorization gate. Unlike S3 bucket policies, an IAM policy alone is never sufficient for KMS. If the key policy does not explicitly permit the IAM system to delegate access (via the root principal statement), all IAM-level grants are silently voided. Your Lambda, EC2 instance profile, ECS task role — all dead on arrival.
The Attack Vector / Blast Radius
This is a self-inflicted denial-of-service on your own encryption layer. The blast radius depends on what the key protects:
- RDS storage encryption: Database remains online but any attempt to rotate credentials or restore a snapshot fails.
- Secrets Manager / Parameter Store: Every application bootstrap that fetches a secret throws
AccessDeniedException— cascading cold-start failures across all services at once. - S3 SSE-KMS: Object PUT/GET operations fail for every requester. If your data pipeline writes to S3, it stalls silently or throws and drops records depending on error handling.
- EBS volumes: Instance stop/start cycles that trigger re-encryption will fail, potentially bricking an autoscaling group during a scale-in event.
From an adversarial angle: if an attacker gains kms:PutKeyPolicy on a key (via a compromised admin role), they can remove the root delegation statement intentionally — effectively destroying your team's ability to recover the key without AWS Support intervention. This is a known ransomware-adjacent technique in cloud environments.
How to Fix It (The Solution)
Basic Fix — Restore the Root Delegation Statement
Every KMS key policy must contain this statement. Without it, no IAM entity in the account can use the key regardless of their IAM policies.
{
"Version": "2012-10-17",
"Statement": [
+ {
+ "Sid": "EnableRootIAMDelegation",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::123456789012:root"
+ },
+ "Action": "kms:*",
+ "Resource": "*"
+ },
{
"Sid": "AllowAppRoleDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyAppRole"
},
- "Action": "kms:*",
+ "Action": [
+ "kms:Decrypt",
+ "kms:DescribeKey"
+ ],
"Resource": "*"
}
]
}
Enterprise Best Practice — Least Privilege with Condition Keys
Granting kms:* to root is the delegation mechanism, not a usage grant. Lock down actual usage with condition keys and separate key administrators from key users.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnableRootIAMDelegation",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowKeyAdmins",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/KMSAdminRole"
},
"Action": [
"kms:Create*", "kms:Describe*", "kms:Enable*",
"kms:List*", "kms:Put*", "kms:Update*",
"kms:Revoke*", "kms:Disable*", "kms:Get*",
"kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "AllowAppRoleEncryptDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyAppRole"
},
"Action": ["kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
"Resource": "*",
+ "Condition": {
+ "StringEquals": {
+ "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com",
+ "aws:PrincipalAccount": "123456789012"
+ }
+ }
}
]
}
kms:ViaService ensures the role can only use the key when the request originates from Secrets Manager — not from a rogue CLI session using the same role.
💡 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
1. Checkov — catch missing root delegation at plan time:
# .checkov.yml
checks:
- CKV_AWS_7 # Ensure KMS key rotation is enabled
- CKV2_AWS_64 # Ensure KMS key policy is not overly permissive
Checkov rule CKV2_AWS_64 will flag key policies where the root principal is absent or where kms:* is granted to "*" without conditions.
2. OPA / Conftest policy for Terraform plan JSON:
package kms.key_policy
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_kms_key"
policy := json.unmarshal(resource.change.after.policy)
statements := policy.Statement
not any_root_delegation(statements)
msg := sprintf("KMS key '%v' is missing root IAM delegation statement", [resource.address])
}
any_root_delegation(statements) {
s := statements[_]
s.Effect == "Allow"
contains(s.Principal.AWS, ":root")
s.Action == "kms:*"
}
3. AWS Config Rule:
Enable managed rule kms-cmk-not-scheduled-for-deletion and pair it with a custom Config rule using kms:GetKeyPolicy to assert root delegation presence on every key in the account. Trigger: ConfigurationItemChangeNotification on AWS::KMS::Key.
4. Terraform — always use the aws_kms_key_policy resource separately from aws_kms_key to avoid accidental policy overwrites during key rotation:
- resource "aws_kms_key" "app" {
- policy = data.aws_iam_policy_document.combined.json
- }
+ resource "aws_kms_key" "app" {
+ enable_key_rotation = true
+ }
+
+ resource "aws_kms_key_policy" "app" {
+ key_id = aws_kms_key.app.id
+ policy = data.aws_iam_policy_document.combined.json
+ }
This separation ensures a terraform apply that modifies key metadata never silently resets the policy to a default that drops your root delegation statement.