MediumThe AWS Feature That Changes How You Debug AccessDenied | by Elad hirsch | Medium
Sitemap

The AWS Feature That Changes How You Debug AccessDenied

9 min readFeb 17, 2026

--

Press enter or click to view image in full size

Introduction

If you have worked with AWS long enough, you have a relationship with AccessDenied. It shows up in Terraform plans, CI/CD pipelines, Lambda logs, and always at the worst possible moment.

This is not surprising. The AWS IAM User Guide is 4k+ pages, and the policy evaluation engine checks up to seven distinct layers before making an access decision. Engineers consistently run out of time and patience before achieving least privilege — and when something breaks, the feedback is even worse.

The error itself is not the problem. The problem is that AccessDenied never told you why. You knew the action was blocked. You knew the resource. But you did not know which of the half-dozen policy layers in your AWS environment was responsible. Was it the identity policy? The permission boundary? An SCP in your organization? A resource policy on the target?

And just few weeks ago in end of January 2026, AWS quietly changed this. Access denied error messages now include the ARN of the specific policy that caused the denial — for same-account and same-organization scenarios. This article walks through why this matters, what it looks like in practice, and provides a Terraform module you can deploy in five minutes to see the enhanced errors yourself.

The Debugging Nightmare

AWS evaluates every API request against multiple policy layers. When any layer denies access, the request fails. The challenge is that there are many layers, and before this change, the error message did not tell you which one was responsible.

The Policy Evaluation Layers

When you make an AWS API call, the IAM engine checks:

  1. Service Control Policies (SCPs) — organization-level guardrails
  2. Resource Control Policies (RCPs) — organization-level resource restrictions
  3. Identity-based policies — policies attached to your IAM user or role
  4. Resource-based policies — policies attached to the target resource (S3 bucket policy, KMS key policy, etc.)
  5. Permission boundaries — maximum permission limits attached to an IAM entity
  6. Session policies — temporary restrictions applied during role assumption
  7. VPC endpoint policies — network-level restrictions on API access

The evaluation rule is state that explicit Deny wins, always. If any single layer explicitly denies an action, the request fails regardless of what every other layer allows.

Press enter or click to view image in full size
Source: AWS IAM documentation — Policy evaluation logic

What the Error Looked Like

Before January 2026, a typical error looked like this:

An error occurred (UnauthorizedOperation) when calling the TerminateInstances operation:
You are not authorized to perform this operation.

That is all you got. No policy name. No layer. No hint. From this message, the denial could have been caused by:

  • An SCP on the account’s organizational unit
  • A permission boundary on the role
  • An explicit Deny in one of the role’s identity policies
  • A resource policy
  • A VPC endpoint policy
  • A session policy applied during AssumeRole

The Old Way using decode-authorization-message

The only diagnostic tool available was aws sts decode-authorization-message. When EC2 returned an encoded message, you could decode it — and get something like this:

{
"allowed": false,
"explicitDeny": true,
"matchedStatements": {
"items": [
{
"statementId": "DenyTerminateInstances",
"effect": "DENY",
"principals": {
"items": [{ "value": "AIDACKCEVSQ6C2EXAMPLE" }]
},
"policyArn": "arn:aws:iam::123456789012:policy/access-denied-demo-deny-terminate",
"actions": {
"items": [{ "value": "ec2:TerminateInstances" }]
},
"resources": {
"items": [{ "value": "arn:aws:ec2:us-east-1:123456789012:instance/*" }]
}
}
]
},
"failures": {},
"context": {
"principal": {
"id": "AROAEXAMPLE:scenario1",
"arn": "arn:aws:sts::123456789012:assumed-role/access-denied-demo-identity-role/scenario1"
},
"action": "ec2:TerminateInstances",
"resource": "arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456789",
"conditions": {
"items": [
{ "key": "ec2:InstanceMarketType", "values": { "items": [{ "value": "on-demand" }] } },
{ "key": "aws:Region", "values": { "items": [{ "value": "us-east-1" }] } },
{ "key": "aws:Resource", "values": { "items": [{ "value": "instance/i-0abc123def456789" }] } },
{ "key": "ec2:InstanceType", "values": { "items": [{ "value": "t3.nano" }] } },
{ "key": "ec2:ResourceTag/Name", "values": { "items": [{ "value": "access-denied-demo-test-instance" }] } }
]
}
}
}

The information is there — buried in a JSON blob with condition keys, nested objects, and principal IDs instead of readable ARNs. And this only works for EC2. Most other services did not provide encoded messages at all.

The decoded message provides forensic depth — statement IDs, x condition keys (instance type, AZ, tags, metadata settings), nested objects, and principal IDs. But it does NOT include the policy ARN. You still need to manually search through every attached policy to find which one contains the statement `"Sid": "DenyTerminateInstances"`. And this only works for EC2 — most other services did not provide encoded messages at all.

The enhanced error gives you what the decoded message never could: the direct path to the blocking policy.

The Manual investigation

Without decode-authorization-message (or when the service did not support it), debugging meant checking every policy layer manually:

# Step 1: Check identity policies on the role
aws iam list-attached-role-policies --role-name MyRole
aws iam list-role-policies --role-name MyRole
# Read each policy, search for Deny statements...

# Step 2: Check permission boundary
aws iam get-role --role-name MyRole --query 'Role.PermissionsBoundary'
# If boundary exists, read the policy, check for Deny...

# Step 3: Check resource policy
aws s3api get-bucket-policy --bucket my-bucket
# Read the bucket policy, look for Deny statements...

# Step 4: Check SCPs (need Organizations access)
aws organizations list-policies-for-target \
--target-id <account-id> \
--filter SERVICE_CONTROL_POLICY
# For each SCP, read the policy document...

# Step 5: Check RCPs
aws organizations list-policies-for-target \
--target-id <account-id> \
--filter RESOURCE_CONTROL_POLICY
# For each RCP, read the policy document...

# Step 6: Check VPC endpoint policies (if applicable)
aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].PolicyDocument'
# Read each endpoint policy...

In a typical enterprise environment with multiple SCPs, boundary policies, and resource policies, this investigation could take thirty minutes to several hours. Multiply that across teams and incidents, and IAM debugging becomes one of the largest hidden costs in cloud operations.

With enhanced error gessages

AWS now includes the policy ARN directly in the error message when an explicit deny blocks the request. The format:

User: <user-arn> is not authorized to perform: <action> on resource: <resource-arn>
with an explicit deny in a <policy-type>: <policy-arn>

Error Examples by Policy Type

Identity-based policy:

User: arn:aws:sts::123456789012:assumed-role/my-role/session is not authorized
to perform: ec2:TerminateInstances on resource:
arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456789
with an explicit deny in an identity-based policy:
arn:aws:iam::123456789012:policy/deny-terminate

Permission boundary:

User: arn:aws:sts::123456789012:assumed-role/my-role/session is not authorized
to perform: ec2:StopInstances on resource:
arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456789
with an explicit deny in a permissions boundary:
arn:aws:iam::123456789012:policy/my-boundary

Service Control Policy:

User: arn:aws:iam::123456789012:user/John is not authorized to perform:
kms:CreateKey with an explicit deny in a service control policy:
arn:aws:organizations::777788889999:policy/o-exampleorgid/service_control_policy/p-scp123

Session policy:

User: arn:aws:iam::123456789012:user/John is not authorized to perform:
dynamodb:PutItem with an explicit deny in a session policy:
arn:aws:iam::123456789012:policy/CISessionPolicy

Try It Yourself , Terraform Demo Module

To see the enhanced errors in your own account, I have created a Terraform module that sets up two scenarios in under five minutes. The module creates IAM roles, policies, and a small EC2 instance (t3.nano) to trigger real errors against.

Prerequisites

  • An AWS account with IAM permissions to create roles, policies, and EC2 instances
  • Terraform 1.5+
  • AWS CLI v2 configured with credentials

Deploy

git clone https://github.com/awslabs/iam-policy-autopilot.git
terraform init
terraform apply

Scenario 1 , Identity-Based Explicit Deny

This role has two policies attached:

  • An inline policy that allows ec2:Describe*, ec2:TerminateInstances, and ec2:StopInstances
  • A managed policy that explicitly denies ec2:TerminateInstances

The Allow and Deny conflict. Explicit Deny wins — but before January 2026, the error never told you which policy was the blocker.

# Assume the role
CREDS=$(aws sts assume-role \
--role-arn $(terraform output -raw identity_role_arn) \
--role-session-name scenario1 \
--query 'Credentials' --output json)

export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .SessionToken)
INSTANCE_ID=$(terraform output -raw test_instance_id)

# This succeeds — Describe* is allowed by the identity policy
aws ec2 describe-instances --instance-ids $INSTANCE_ID --region us-east-1

# This fails — TerminateInstances is explicitly denied
aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region us-east-1 2>&1

You will then see the enhanced error:

An error occurred (UnauthorizedOperation) when calling the TerminateInstances operation:
You are not authorized to perform this operation. Encoded authorization failure message:
...
User: arn:aws:sts::123456789012:assumed-role/access-denied-demo-identity-role/scenario1
is not authorized to perform: ec2:TerminateInstances on resource:
arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456789
with an explicit deny in an identity-based policy:
arn:aws:iam::123456789012:policy/access-denied-demo-deny-terminate

The error points directly to the guardrail policy. No manual investigation needed.

# Clean up credentials before next scenario
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

Scenario 2 , The Multi-Layer Nightmare

This is the scenario that used to take an hour to debug. A single role with three policy layers:

  • Identity policy: allows ec2:*
  • Permission boundary: allows most actions but explicitly denies ec2:StopInstances
  • Managed policy: explicitly denies ec2:TerminateInstances

Two different actions fail for two completely different reasons.Before January 2026, both said the same thing: “You are not authorized to perform this operation.”

# Assume the nightmare role
CREDS=$(aws sts assume-role \
--role-arn $(terraform output -raw nightmare_role_arn) \
--role-session-name scenario2 \
--query 'Credentials' --output json)

export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .SessionToken)
INSTANCE_ID=$(terraform output -raw test_instance_id)

# Blocked by IDENTITY POLICY:
aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region us-east-1 2>&1

# You will see something like - explicit deny in an identity-based policy:
# arn:aws:iam::123456789012:policy/access-denied-demo-deny-terminate

# Blocked by PERMISSION BOUNDARY:
aws ec2 stop-instances --instance-ids $INSTANCE_ID --region us-east-1 2>&1

# You will see something like - with an explicit deny in a permissions boundary:
# arn:aws:iam::123456789012:policy/access-denied-demo-boundary

So it’s the same role but two different errors. Two different policy layers identified in the error message. Each one tells you exactly where to look.

Get Elad hirsch’s stories in your inbox

Join Medium for free to get updates from this writer.

For comparison, the decode-authorization-message output for the boundary denial shows the principal as SCOPE_POLICY_ISSUER — a cryptic identifier that tells you a boundary was involved, but not which one. The enhanced error gives you the exact ARN.

Clean Up

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
terraform destroy -auto-approve

Note: The module creates a t3.nano EC2 instance. Run terraform destroy when you are done to avoid charges .
p.s — can you guess what is the yearly cost of a t3.nano in us-east-1 for a year without checking in AWS Pricing Calculator :-) ?

What This Changes for Operations Teams

Shorter Incident Response

The manual “policy archaeology” workflow collapses. Instead of checking every layer, read the error, go to the policy, fix it. What took thirty minutes to an hour now takes two minutes.

Safer Layered IAM Architecture

Many organizations avoid layering SCPs, permission boundaries, and identity policies because debugging conflicts was too painful. Now that the error message identifies the blocking layer, teams can use defense-in-depth IAM architectures with confidence.

Better operational runbooks

If the error mentions service control policy → contact the platform team that manages SCPs in AWS Organizations

  • If the error mentions permissions boundary → contact the team that manages IAM boundaries
  • If the error mentions identity-based policy → review the role’s attached policies
  • If the error mentions session policy → check the CI/CD system that applied session restrictions during role assumption

Log Enrichment

Since the policy ARN appears in the error message (and in CloudTrail), log pipelines can extract it as a structured field:

# Example: parse policy ARN from CloudTrail AccessDenied events
import re
pattern = r'in a (\w[\w\s-]*): (arn:aws:[^\s]+)'
match = re.search(pattern, error_message)
if match:
policy_type = match.group(1) # "permissions boundary"
policy_arn = match.group(2) # "arn:aws:iam::123456789012:policy/..."

Dashboards can group AccessDenied events by policy ARN, alerting on spikes tied to a specific guardrail.

Limitations to Know

Same-account and same-organization only. Cross-account requests outside your AWS Organization still return generic “Access Denied.” Cross-account debugging remains manual.

Explicit deny focus. The enhanced messages are most useful when an explicit Deny statement blocks the request. A missing Allow (implicit deny) produces a different message format:

User: <arn> is not authorized to perform: <action>
because no identity-based policy allows the <action> action

This tells you the policy type but does not include a specific ARN, since no single policy is responsible — the permission simply does not exist.

Gradual rollout. AWS is rolling this out across services and regions over time. In our testing, EC2 consistently shows enhanced errors, while S3 and DynamoDB does not yet surface boundary denials in the same detail.

Does not cover VPC endpoint policies. If a VPC endpoint policy blocks the request, the error message does not currently include the endpoint policy ARN.

Try the demo: access-denied-demo/

Elad Hirsch is Head of Public Cloud and AI at TeraSky, where he helps organizations build cloud-native solutions and navigate the AI landscape. With a background in software architecture and DevOps, he’s particularly interested in the practical applications of generative AI — and the problems that come with them.

--

--

Elad hirsch
Elad hirsch

Head of Public Cloud and AI | Driving Cloud & AI Business Strategy| Building High-Performance Teams | AWS Ambassador