Initializing Enclave...

Fixing Cross-Region AssumeRole AccessDenied Caused by a KMS Key Region Mismatch

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins


TL;DR

  • What broke: An IAM role assumed cross-region is calling kms:Decrypt against a single-region KMS key in us-east-1. The key policy does not authorize the assumed-role principal, or the key is not replicated as a multi-region key, so KMS hard-rejects the call with AccessDenied.
  • How to fix it: Either (a) replicate the KMS key as a multi-region key and reference the replica ARN in the target region, or (b) explicitly grant kms:Decrypt to the cross-region assumed-role principal in the us-east-1 key policy.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your key policy and role trust policy and get a corrected diff without sending your ARNs to a third-party server.

The Incident (What Does the Error Mean?)

The raw error surface looks like this:

An error occurred (AccessDenied) when calling the Decrypt operation:
User: arn:aws:sts::123456789012:assumed-role/cross-region-worker/session-id
is not authorized to perform: kms:Decrypt
on resource: arn:aws:kms:us-east-1:123456789012:key/mrk-abc123def456
because no resource-based policy allows the kms:Decrypt action

Immediate consequence: Any workload relying on that assumed role to decrypt SSM Parameter Store SecureStrings, S3 SSE-KMS objects, Secrets Manager secrets, or RDS encrypted snapshots silently fails or throws a hard exception. If this is in a Lambda cold-start path or an ECS task bootstrap, the entire service is down. The error is often misread as an IAM identity policy problem — it is almost always a KMS key policy gap.


The Attack Vector / Blast Radius

KMS key policies are resource-based policies that must explicitly allow the caller. Unlike S3 bucket policies, KMS does not fall back to identity policy evaluation alone — both the key policy and the identity policy must permit the action. When you AssumeRole cross-region, the resulting STS principal ARN (sts::ACCOUNT:assumed-role/ROLE/SESSION) is a different evaluated principal than the base role ARN. If the key policy was written against the base role or against an account root without a matching condition, the assumed-role session is denied.

Blast radius breakdown:

Affected Layer Impact
Secrets Manager All secret reads fail; app cannot retrieve DB credentials
S3 SSE-KMS GetObject succeeds but decryption fails — data is inaccessible
EBS/RDS encrypted volumes Snapshot restore and cross-region copy operations blocked
SSM SecureString Parameter reads return AccessDenied, breaking bootstrap scripts
CloudTrail Every failed Decrypt attempt is logged — high-volume noise that buries real attack signals

Security misconfig angle: If engineers respond to this error by adding kms:* to the key policy for "Principal": "*" or "Principal": {"AWS": "*"} to stop the outage fast, they have created a publicly accessible KMS key. Any authenticated AWS principal in any account can now decrypt your data. This is a critical data exfiltration vector.


How to Fix It (The Solution)

Basic Fix — Add the Assumed-Role Principal to the KMS Key Policy

Navigate to the KMS key in us-east-1Key Policy → Edit. Add the cross-region role as an explicit principal.

 {
   "Version": "2012-10-17",
   "Statement": [
     {
       "Sid": "AllowDecryptForWorkloads",
       "Effect": "Allow",
       "Principal": {
         "AWS": [
-          "arn:aws:iam::123456789012:role/same-region-worker"
+          "arn:aws:iam::123456789012:role/same-region-worker",
+          "arn:aws:iam::123456789012:role/cross-region-worker"
         ]
       },
       "Action": [
         "kms:Decrypt",
         "kms:DescribeKey"
       ],
       "Resource": "*"
     }
   ]
 }

⚠️ This works but does not scale. Every new cross-region role requires a manual key policy edit.


Enterprise Best Practice — Multi-Region KMS Key with Condition-Locked Policy

The correct long-term fix is to promote the key to a multi-region key (or create a new MRK) and replicate it to the target region. The consuming workload references the replica key ARN in its own region, eliminating the cross-region KMS API call entirely. Lock the key policy with aws:PrincipalOrgID and aws:RequestedRegion conditions to prevent lateral abuse.

Step 1 — Enable multi-region replication (Terraform):

 resource "aws_kms_key" "app_key" {
   description             = "Application encryption key"
   deletion_window_in_days = 30
   enable_key_rotation     = true
-  multi_region            = false
+  multi_region            = true
 }

+resource "aws_kms_replica_key" "app_key_replica" {
+  description             = "Replica of app_key in eu-west-1"
+  primary_key_arn         = aws_kms_key.app_key.arn
+  provider                = aws.eu_west_1
+  deletion_window_in_days = 30
+}

Step 2 — Harden the key policy with org and region conditions:

 {
   "Sid": "AllowDecryptWithGuardrails",
   "Effect": "Allow",
   "Principal": {
     "AWS": "arn:aws:iam::123456789012:root"
   },
   "Action": [
     "kms:Decrypt",
     "kms:DescribeKey"
   ],
   "Resource": "*",
   "Condition": {
+    "StringEquals": {
+      "aws:PrincipalOrgID": "o-xxxxxxxxxx",
+      "aws:PrincipalTag/Environment": "production"
+    },
+    "StringLike": {
+      "aws:PrincipalArn": "arn:aws:iam::123456789012:role/cross-region-worker"
+    },
-    "Bool": {
-      "aws:SecureTransport": "true"
-    }
+    "Bool": {
+      "aws:SecureTransport": "true",
+      "aws:MultiFactorAuthPresent": "false"
+    }
   }
 }

Step 3 — Update the consuming workload to reference the replica ARN, not the primary:

 # In your application config or Terraform variable
-kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/mrk-abc123def456"
+kms_key_arn = "arn:aws:kms:eu-west-1:123456789012:key/mrk-abc123def456"

The workload now calls KMS in its own region against the replica. No cross-region KMS API traversal. Latency drops. The AccessDenied disappears.


💡 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

This class of error is 100% preventable at the policy-authoring stage. Wire these checks into your pipeline before any KMS or IAM change merges.

1. Checkov — Detect single-region KMS keys used cross-region

Add a custom Checkov check or use built-in rules:

# .checkov.yml
checks:
  - CKV_AWS_7   # Ensure KMS key rotation is enabled
  - CKV2_AWS_64 # Ensure KMS key policy is not overly permissive
  - CKV_AWS_219 # Ensure KMS Multi-Region key is used where cross-region access is required

Run in CI:

checkov -d ./terraform \
  --framework terraform \
  --check CKV_AWS_7,CKV2_AWS_64,CKV_AWS_219 \
  --hard-fail-on HIGH

2. OPA / Conftest — Block key policies with wildcard principals

# policy/kms_no_wildcard_principal.rego
package kms

deny[msg] {
  stmt := input.Statement[_]
  stmt.Effect == "Allow"
  stmt.Principal == "*"
  msg := sprintf(
    "KMS key policy statement '%v' grants access to wildcard principal — forbidden.",
    [stmt.Sid]
  )
}

deny[msg] {
  stmt := input.Statement[_]
  stmt.Effect == "Allow"
  stmt.Principal.AWS == "*"
  msg := sprintf(
    "KMS key policy statement '%v' grants AWS wildcard principal — forbidden.",
    [stmt.Sid]
  )
}
conftest test kms_key_policy.json --policy policy/

3. AWS Config Rule — Continuous drift detection

Deploy the managed rule kms-cmk-not-scheduled-for-deletion and a custom Config rule that flags any KMS key policy lacking aws:PrincipalOrgID as a condition when the principal is not the account root. Trigger an SNS alert to your security channel on any non-compliant change.

4. Terraform prevent_destroy + tagging enforcement

 resource "aws_kms_key" "app_key" {
   description  = "Application encryption key"
   multi_region = true
+
+  lifecycle {
+    prevent_destroy = true
+  }
+
+  tags = {
+    CrossRegionReplication = "enabled"
+    ManagedBy              = "terraform"
+    SecurityReviewed       = "true"
+  }
 }

A CrossRegionReplication = "enabled" tag triggers your pipeline's policy gate to verify a matching aws_kms_replica_key resource exists in every declared target region before terraform apply proceeds.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →