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:Decryptagainst a single-region KMS key inus-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 withAccessDenied. - 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:Decryptto the cross-region assumed-role principal in theus-east-1key 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-1 → Key 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.