AWS Access Denied: 7 Causes & STS Decode
AWS Access Denied errors occur when an IAM policy evaluation denies an API request. AWS evaluates requests through a chain of up to 7 policy types - SCPs, permission boundaries, session policies, identity policies, resource policies, VPC endpoint policies, and condition keys. Starting January 2026, AWS error messages include the specific policy ARN that blocked the request. For encoded authorization failures, the aws sts decode-authorization-message command decodes the error into a readable JSON showing the exact action, resource, and policy that denied access.
“Access Denied.”
That’s it. No policy name. No ARN. No hint about which of the 14 possible evaluation steps said no. Just… Access Denied.
I’ve spent more hours debugging this error than any other in AWS. Not because it’s technically complex - it’s not. It’s because AWS historically gave you nothing to work with. You’d stare at an IAM policy that looked correct, the resource policy that looked correct, and somewhere in the chain of SCPs, permission boundaries, session policies, VPC endpoint policies, and condition keys, something was saying no. Good luck figuring out which one.
The good news: AWS shipped a feature in January 2026 that changes this. The bad news: most engineers don’t know it exists yet, and it doesn’t cover every scenario. So you still need a systematic approach.
Here’s mine.
The 2026 change: error messages that actually help
Starting January 2026, AWS Access Denied errors now include the ARN of the specific policy that blocked your request. Not just the type - the actual ARN.
Before:
User: arn:aws:iam::123456789012:user/deploy-bot is not authorized
to perform: iam:ListRoles on resource: arn:aws:iam::123456789012:role/*
with an explicit deny in a service control policy
After:
User: arn:aws:iam::123456789012:user/deploy-bot is not authorized
to perform: iam:ListRoles on resource: arn:aws:iam::123456789012:role/*
with an explicit deny in a service control policy:
arn:aws:organizations::987654321098:policy/o-qv5af4abcd/service_control_policy/p-2kgnabcd
That last line is new. You get the exact SCP, permission boundary, or identity policy that blocked the request. You can go straight to it instead of hunting through 30 policies.
This is rolling out gradually across all AWS services and regions throughout early 2026. You might already have it. If you don’t see it yet, the old debugging approach still applies - and that’s what the rest of this article is about.
One caveat: the policy ARN only shows up for same-account or same-organization requests. Cross-account calls from outside your org won’t include it, for security reasons.
The 7 places IAM actually checks
When you make an AWS API call, IAM doesn’t just check “does this user have an Allow?” It runs through a chain of evaluations. If any one of them says no, you get Access Denied. And the error message (pre-2026) usually doesn’t tell you which one.
Here’s every place a denial can come from, in order of how often I see them cause confusion.
1. Service Control Policies (SCPs)
This is the #1 “invisible wall” I encounter in enterprise environments.
SCPs are set at the AWS Organizations level - on the org root, an OU, or a specific account. They don’t grant permissions. They set the maximum boundary of what’s allowed. If an SCP doesn’t explicitly allow an action, it’s implicitly denied - even if the IAM user has AdministratorAccess.
The error looks like this:
User: arn:aws:iam::111111111111:user/admin is not authorized to perform:
ec2:RunInstances with an explicit deny in a service control policy
The frustrating part: member accounts can’t see which SCPs apply to them. Only the management account can view SCPs. So if you’re a developer in a member account, you might have full admin permissions and still get Access Denied, with no way to check why from your own account.
How to debug:
# From the management account - list SCPs on the target account
aws organizations list-policies-for-target \
--target-id 111111111111 \
--filter SERVICE_CONTROL_POLICY
# Get the policy content
aws organizations describe-policy --policy-id p-abc123def
If you don’t have access to the management account, ask your org admin. There’s no other way.
Common SCP traps:
- Region restrictions: SCP that denies all actions outside
eu-central-1andus-east-1(you need us-east-1 for global services like IAM, CloudFront, Route53) - Service blocklists: SCP that denies expensive services like SageMaker, Redshift - but also catches related actions you didn’t expect
- The “deny all except” pattern: if someone writes an SCP with explicit Deny + NotAction, every new AWS service is denied by default until someone updates the SCP
2. Permission boundaries
Permission boundaries are IAM policies attached to a user or role that limit the maximum permissions. They work like SCPs but at the identity level.
Here’s why they’re confusing: a user can have AdministratorAccess as their identity policy AND a permission boundary. The effective permissions are the intersection - only actions allowed by both policies work. Everything else is implicitly denied.
User: arn:aws:iam::111111111111:role/dev-role is not authorized to perform:
s3:DeleteBucket with an implicit deny in a permissions boundary
How to debug:
# Check if the role has a permission boundary
aws iam get-role --role-name dev-role \
--query 'Role.PermissionsBoundary'
# If it returns an ARN, inspect that policy
aws iam get-policy-version \
--policy-arn arn:aws:iam::111111111111:policy/dev-boundary \
--version-id $(aws iam get-policy --policy-arn arn:aws:iam::111111111111:policy/dev-boundary --query 'Policy.DefaultVersionId' --output text)
If PermissionsBoundary is null, this isn’t your problem. If it returns an ARN, that policy is probably your blocker.
Real-world example: I see this a lot in organizations that use SSO with permission sets. The admin creates a role with broad permissions but attaches a boundary that blocks iam:*, organizations:*, and account:*. Developers can do everything except IAM changes - but the error message just says “Access Denied” without mentioning the boundary.
3. Resource policies
This one catches people who think IAM is just about identity policies. It’s not.
Some AWS services have their own policies attached directly to the resource:
- S3 bucket policies
- KMS key policies
- SQS queue policies
- SNS topic policies
- Lambda function policies
- ECR repository policies
- Secrets Manager resource policies
- API Gateway resource policies
Resource policies can explicitly deny access regardless of what the identity policy allows. A Deny in a resource policy wins over an Allow in an IAM policy. Always.
User: arn:aws:iam::111111111111:role/app-role is not authorized to perform:
s3:GetObject on resource: arn:aws:s3:::my-bucket/config.json
How to debug:
# S3 bucket policy
aws s3api get-bucket-policy --bucket my-bucket --output text | jq .
# KMS key policy
aws kms get-key-policy --key-id abc-123 --policy-name default --output text | jq .
# SQS queue policy
aws sqs get-queue-attributes --queue-url https://sqs.eu-central-1.amazonaws.com/111111111111/my-queue \
--attribute-names Policy --query 'Attributes.Policy' --output text | jq .
Look for "Effect": "Deny" statements. Pay special attention to NotPrincipal - it denies everyone EXCEPT the listed principals.
4. KMS dual authorization
This one deserves its own section because it trips up even experienced engineers.
KMS keys use a dual authorization model: both the key policy AND the IAM policy must allow the action. Unlike S3 where an identity policy alone can grant access, KMS requires explicit permission in the key policy.
An error occurred (AccessDeniedException) when calling the Decrypt operation:
The cipherbase is not authorized to perform: kms:Decrypt on the resource: arn:aws:kms:...
This happens when:
- Your IAM role has
kms:Decryptpermission - But the KMS key policy doesn’t include your role as a principal
How to debug:
# Check the key policy
aws kms get-key-policy --key-id YOUR_KEY_ID --policy-name default --output text | jq .
Look for your role/user ARN in the Principal field. If it’s not there, the key policy needs to be updated - no amount of IAM policy changes will fix it.
The shortcut: if the key policy contains this statement, it delegates authorization to IAM policies:
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:root"},
"Action": "kms:*",
"Resource": "*"
}
This root principal statement means “trust IAM policies in this account to grant KMS access.” If this statement is missing or restricted, you need explicit key policy grants.
5. VPC endpoint policies
If your workload runs in a private subnet and accesses AWS services through a VPC endpoint, the endpoint itself can have a policy that restricts what’s allowed.
This is a particularly nasty one because the error looks identical to a regular Access Denied - nothing in the message hints that a VPC endpoint is involved.
An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
How to debug:
# List VPC endpoints
aws ec2 describe-vpc-endpoints \
--filters "Name=service-name,Values=com.amazonaws.eu-central-1.s3" \
--query 'VpcEndpoints[].[VpcEndpointId,PolicyDocument]'
If the policy is anything other than the default (full access), it might be restricting your request. Common restrictions:
- Only specific buckets allowed
- Only specific principals allowed
- Only specific actions allowed
How I found this once: an app worked fine from a developer’s laptop but failed with Access Denied when deployed to ECS in a private subnet. Same IAM role, same bucket, same code. The only difference was the network path - ECS traffic went through a VPC endpoint with a policy that only allowed access to three specific buckets. The app was trying to reach a fourth one.
6. Session policies
When you assume a role with sts:AssumeRole, you can optionally pass a session policy that further restricts the role’s permissions for that session.
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/broad-role \
--role-session-name my-session \
--policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'
That --policy parameter limits this specific session to only s3:GetObject, even if the role itself has full admin. The effective permissions are the intersection of the role’s policies and the session policy.
How to debug:
Session policies don’t leave an obvious trace. The role’s policies look fine, the resource policy looks fine, but the actual session has been scoped down.
# Check what the current session can actually do
aws sts get-caller-identity
# If the RoleSessionName looks auto-generated or unusual, someone might
# be passing a session policy during AssumeRole
Check your application code or CI/CD pipeline for --policy parameters in AssumeRole calls. Also check if you’re using AWS SSO/Identity Center - some permission sets configure session policies.
CloudTrail is your friend here. Look for the AssumeRole event and check the requestParameters for a policy field:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--max-results 5
7. Condition keys
IAM policies support conditions that restrict when an Allow or Deny applies. These are silent killers because the policy looks like it grants access - until you read the fine print.
Common conditions that cause unexpected denials:
IP restriction:
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
Works from the office. Fails from home VPN. Fails from Lambda. Fails from any AWS service making calls on your behalf.
MFA requirement:
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "true"
}
}
Works in the console (MFA at login). Fails with CLI access keys that don’t have MFA session tokens.
Tag-based access control (ABAC):
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": "${aws:PrincipalTag/Environment}"
}
}
Only works if both the resource AND the principal have matching Environment tags. Missing a tag on either side = Access Denied.
Time-based restrictions:
"Condition": {
"DateLessThan": {
"aws:CurrentTime": "2026-03-01T00:00:00Z"
}
}
Temporary access that expired. The policy still exists, still looks valid - but the condition quietly blocks everything.
How to debug:
Conditions don’t show up in the error message. You need to read the actual policy JSON and check every condition block. Use IAM Policy Simulator to test with specific context:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111111111111:role/my-role \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/file.txt \
--context-entries "ContextKeyName=aws:SourceIp,ContextKeyValues=192.168.1.1,ContextKeyType=ip"
Quick reference: where to check for each denial type
| Denial Source | Error Hint | Debug Command | Common Cause |
|---|---|---|---|
| SCP | ”explicit deny in a service control policy” | aws organizations list-policies-for-target | Region restrictions, service blocklists |
| Permission Boundary | ”implicit deny in a permissions boundary” | aws iam get-role --query PermissionsBoundary | SSO permission sets with boundaries |
| Resource Policy | Generic “Access Denied” | aws s3api get-bucket-policy / aws kms get-key-policy | Explicit Deny or missing principal |
| KMS Key Policy | ”not authorized to perform: kms:Decrypt” | aws kms get-key-policy | Missing root delegation statement |
| VPC Endpoint | Generic “Access Denied” (from private subnet) | aws ec2 describe-vpc-endpoints | Restrictive endpoint policy |
| Session Policy | Generic “Access Denied” (role looks correct) | Check AssumeRole call for —policy parameter | CI/CD passing session policies |
| Condition Keys | Generic “Access Denied” (policy looks correct) | aws iam simulate-principal-policy with context | IP restrictions, MFA, tag mismatches |
The trick most engineers don’t know: DecodeAuthorizationMessage
Some AWS services (especially EC2) return an encoded authorization message instead of a readable error:
A]n error occurred (UnauthorizedOperation) when calling the RunInstances operation:
You are not authorized to perform this operation. Encoded authorization failure message:
eTDFi1nQHZWuYb4Fz5f4k5LEXAMPLE...
That encoded blob actually contains exactly what went wrong - which action, which resource, which policy said no, and which conditions failed. But you have to decode it:
aws sts decode-authorization-message \
--encoded-message "eTDFi1nQHZWuYb4Fz5f4k5LEXAMPLE..." \
--query DecodedMessage --output text | jq .
The decoded JSON tells you:
allowed: whether the request was allowed (false)explicitDeny: whether it was an explicit Deny or just no AllowmatchedStatements: which policy statements were evaluatedcontext: the action, resource, and all condition key values
One catch: you need sts:DecodeAuthorizationMessage permission to run this. If the user who got the error doesn’t have this permission, use a more privileged role or the account admin to decode it. Yes - you sometimes need permissions to find out why you don’t have permissions. Welcome to IAM.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:DecodeAuthorizationMessage",
"Resource": "*"
}]
}
Add this to your developer roles. It costs nothing and saves hours.
My debugging flowchart
When I get Access Denied, I run through this in order:
Step 1: Read the error message carefully. Since the 2026 update, it might include the policy ARN. If it says “explicit deny in a service control policy” with an ARN - you’re done, go look at that SCP.
Step 2: Check who you actually are.
aws sts get-caller-identity
Seriously. Half the time it’s the wrong role, wrong account, or expired session. I’ve lost count of how many times the “IAM issue” was just the wrong AWS profile in the terminal.
Step 3: Is it an encoded message?
If you see Encoded authorization failure message: - decode it with sts decode-authorization-message. The answer is literally in the blob.
Step 4: Check identity policies.
# For a role
aws iam list-attached-role-policies --role-name my-role
aws iam list-role-policies --role-name my-role
# For a user
aws iam list-attached-user-policies --user-name my-user
aws iam list-user-policies --user-name my-user
# Don't forget group policies
aws iam list-groups-for-user --user-name my-user
Step 5: Check permission boundaries.
aws iam get-role --role-name my-role --query 'Role.PermissionsBoundary'
If this returns an ARN, the effective permissions are the intersection of identity policies and the boundary.
Step 6: Check resource policies. S3 bucket policy, KMS key policy, SQS queue policy - whatever service you’re calling. An explicit Deny there overrides everything.
Step 7: Check SCPs. You need management account access. Ask your org admin to run:
aws organizations list-policies-for-target --target-id ACCOUNT_ID --filter SERVICE_CONTROL_POLICY
Step 8: Check VPC endpoint policies. Only relevant if the call is coming from a private subnet with VPC endpoints.
Step 9: Check CloudTrail.
The event will show errorCode: AccessDenied and sometimes additional context about the denial source.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=RunInstances \
--start-time 2026-03-07T10:00:00Z \
--max-results 10
Nine times out of ten, the problem is in steps 1-5. Steps 6-9 are for the stubborn cases.
Prevention beats debugging
After years of cleaning up Access Denied messes, I’ve landed on a few rules:
- Add
sts:DecodeAuthorizationMessageto every developer role. It costs nothing and gives them self-service debugging. - Tag your SCPs clearly. When someone reads an SCP name like
DenyNonEURegions-v2, they’ll understand the denial instantly. When it’s namedp-abc123, they’ll waste an hour. - Use IAM Access Analyzer. It identifies unused permissions, validates policies, and catches common mistakes before they ship.
- Log everything. CloudTrail with management events AND data events (at least for S3/KMS) gives you a complete audit trail when things go wrong.
- Test cross-account access with IAM Policy Simulator. Don’t wait for production to find out your role can’t reach a shared bucket.
IAM is just one piece of the puzzle. If you want a broader look at common security gaps, check my AWS security audit checklist – 17 issues I find in every account. And if your VPC endpoint policies interact with network firewalls, see my comparison of AWS Network Firewall vs Palo Alto VM-Series - including a documented SNI bypass that affects domain allowlists.
Frequently Asked Questions
How do I decode AWS Access Denied error messages?
Use aws sts decode-authorization-message --encoded-message YOUR_ENCODED_MESSAGE --query DecodedMessage --output text | jq . - this decodes the encoded authorization failure into JSON showing the exact action, resource, and policy that denied access. You need sts:DecodeAuthorizationMessage permission to run this command.
What is aws sts decode-authorization-message?
It is an AWS STS API that decodes additional information from encoded authorization failure messages. When AWS services like EC2 return an “Encoded authorization failure message”, this command reveals which policy denied access, the requested action, the target resource, and the condition key values that were evaluated.
Why does my IAM policy show Access Denied when it looks correct?
AWS IAM evaluates requests through 7 policy types in sequence: SCPs, permission boundaries, session policies, identity policies, resource policies, VPC endpoint policies, and condition keys. A denial from any one of these overrides allows from others. The most common hidden causes are SCPs in AWS Organizations, permission boundaries on roles, and condition keys like IP restrictions or MFA requirements.
What changed in AWS Access Denied errors in 2026?
Starting January 2026, AWS Access Denied error messages include the ARN of the specific policy that blocked the request. Previously, errors only mentioned the policy type (e.g., “service control policy”) without identifying which exact policy. This works for same-account and same-organization requests; cross-account calls from outside your org won’t include it.
Struggling with IAM complexity in a multi-account AWS setup? I help teams untangle permission models, audit security configurations, and build IAM structures that don’t require a PhD to debug. Let’s talk.