How to Fix AWS AccessDenied and 'Not Authorized to Perform' Errors (2026)

Oct 31, 2026 · 15 min · Ajay Kumar

This guide is for anyone holding an AccessDenied, AccessDeniedException, UnauthorizedOperation or "is not authorized to perform" error from AWS and wanting the cause, not a wider policy. By the end you will have a repeatable order of checks: read the message (which in 2026 often names the exact policy), prove who you are, decode or look up the evaluation, reproduce it in the policy simulator, find it in CloudTrail, then fix the specific SCP, RCP, boundary, session, trust, bucket or key policy that said no. Commands were checked against the AWS CLI v2 reference (current release 2.38.5, 22 October 2026) and the IAM, S3, KMS and Organizations docs as of 24 October 2026; jq 1.7.1 is used for parsing. You need AWS CLI v2 with working credentials and read access to IAM (iam:Get*, iam:List*, iam:SimulatePrincipalPolicy); organization steps need the management account or a delegated administrator. For the concepts behind each step, read the companion explainer, AWS IAM Explained (2026).

Fixing AWS AccessDenied: the order of checks Read the whole message principal, action, resource, policy type, policy ARN Confirm who you are aws sts get-caller-identity account, role, session name What does it say? explicit deny in a X policy, or because no X policy allows Encoded, or has an ID sts decode-authorization-message or access troubleshooter (preview) Bare Access Denied cross-org caller, VPC endpoint, or a service without detail Names a policy type ARN shown: open that policy SCP or RCP: org admin's change Prove the fix, then make the smallest change simulate-principal-policy, validate-policy, retry the real call, check CloudTrail errorCode Still denied? Check the resource side, which the caller's policies cannot fix: S3 bucket policy, KMS key policy, role trust policy, VPC endpoint policy, S3 Object Ownership and ACLs
The checking order this guide follows. Message formats and tool behaviour come from the IAM, STS, S3 and CloudTrail documentation cited in each step.

Step 1: Read the whole AccessDenied message

Most people read the first five words and start adding permissions. The rest of the message is the diagnosis. AWS's troubleshooting guide documents the format: User <principal> is not authorized to perform <action> on <resource> because <context>. The context tells you which policy type failed and whether it was an explicit deny or a missing allow:

# Implicit deny (illustrative): nothing allowed it. Fix by adding an Allow in the named policy type.
User: arn:aws:sts::111122223333:assumed-role/ci-deploy/gha is not authorized to perform: iam:CreateRole
on resource: arn:aws:iam::111122223333:role/app because no permissions boundary allows the iam:CreateRole action

# Explicit deny with the policy ARN (AWS's documented example; added 21 January 2026, rolling out per service).
User: arn:aws:iam::123456789012:user/John is not authorized to perform: codecommit:ListRepositories
with an explicit deny in a service control policy: arn:aws:organizations::777788889999:policy/o-exampleorgid/service_control_policy/p-examplepolicyid123

Since 21 January 2026, explicit denies from SCPs, RCPs, identity-based policies, session policies and permissions boundaries include the denying policy's ARN when the caller and the policy are in the same account or organization. S3 added the same detail to its 403 responses on 13 August 2026, and its 403 troubleshooting page lists the exceptions: cross-organization callers get a bare Access Denied, VPC endpoint denials get no enhanced message, and directory buckets are excluded. A Classmethod test found that inline policies and bucket policies show only the policy type, because they have no ARN. Two more rules from AWS: if several policy types deny, the message names only one, and if several policies of one type deny, it does not say how many.

Phrase in the message Open this Usually owned by
no identity-based policy allows Policies on the user or role You or the app team
explicit deny in an identity-based policy The named managed or inline policy You or the app team
no permissions boundary allows The boundary on the user or role Platform team
no session policy allows The policy passed to AssumeRole Whatever minted the credentials
service control policy SCPs on the account and its OUs Organization admins
resource control policy RCPs on the resource's account and OUs Organization admins
no resource-based policy allows Bucket, key, queue or secret policy Resource owner
role trust policy The target role's trust policy Role owner
VPC endpoint policy The endpoint's policy Network team

Source: phrases from the IAM and S3 troubleshooting pages.

Step 2: Confirm who you are with aws sts get-caller-identity

In my experience a good share of AccessDenied errors are correct: the request came from a different principal than you assumed. An environment variable from last week's debugging, a CI runner using its instance role instead of the OIDC role, or a profile pointing at the wrong account will all do it.

# Who is AWS actually seeing? Works with any valid credentials; needs no IAM permission.
aws sts get-caller-identity
# {
#     "UserId": "AROAEXAMPLEID:jane@example.com",
#     "Account": "111122223333",
#     "Arn": "arn:aws:sts::111122223333:assumed-role/AWSReservedSSO_Developer_0123456789abcdef/jane@example.com"
# }

# Where did those credentials come from? The Type and Location columns show the source.
aws configure list
env | grep -E '^AWS_(PROFILE|ACCESS_KEY_ID|SESSION_TOKEN|REGION|DEFAULT_REGION)='

# Expired Identity Center or console session? Refresh it rather than creating access keys.
aws sso login --profile dev        # Identity Center profiles
aws login --profile dev            # console credentials, AWS CLI 2.32.0 or later

Note the ARN shape. A role session is arn:aws:sts::ACCOUNT:assumed-role/ROLE/SESSION, but IAM APIs and the policy simulator want the role ARN, which can include a path. Identity Center roles live under /aws-reserved/sso.amazonaws.com/, so do not rebuild the ARN by hand:

# Turn the session ARN into the real role ARN (path included) for later steps.
SESSION_ARN=$(aws sts get-caller-identity --query Arn --output text)
ROLE_NAME=$(echo "$SESSION_ARN" | awk -F'[:/]' '$6 == "assumed-role" {print $7}')
ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query Role.Arn --output text)
echo "$ROLE_ARN"
# arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/eu-west-2/AWSReservedSSO_Developer_0123456789abcdef

If the account or role is wrong, stop here. The fix is the credential chain, not IAM.

Step 3: Decode encoded messages and use the authorization ID

EC2 and a few other services return UnauthorizedOperation with an encoded blob instead of readable context. The EC2 error reference points to STS DecodeAuthorizationMessage, which needs the sts:DecodeAuthorizationMessage permission (API reference):

# Paste the blob exactly as returned; line breaks or an expired message give InvalidAuthorizationMessage.
ENCODED='<encoded-message-from-the-error>'
aws sts decode-authorization-message \
  --encoded-message "$ENCODED" \
  --query DecodedMessage --output text | jq '{allowed, explicitDeny, matchedStatements, action: .context.action, resource: .context.resource, conditions: .context.conditions}'

The decoded JSON says whether the denial was explicit ("explicitDeny": "true"), which principal, action and resource were evaluated, and the condition key values AWS saw. That last part is the useful one: a tag value, Region or tenancy you did not expect shows up right there.

AWS is also previewing an access troubleshooter. For supported APIs (so far, most IAM APIs) the denial ends with an authorization ID and a console link, and one call returns every policy evaluated with its result. It arrived in AWS CLI 2.36.31 on 25 August 2026, per the CLI changelog, as a preview in us-east-1. The docs say details are kept for 24 hours, the API is regional, and you need iam:GetRequestAuthorizationDetails:

# Call it in the Region where the denial happened; retry after a few seconds if it returns not found.
aws iam-toolbox get-request-authorization-details \
  --region us-east-1 \
  --authorization-id EXAMPLE1a2b3c4d5e6f7g8h9i0j

Step 4: Reproduce the decision with the IAM policy simulator

Once you know the principal, action and resource, prove the decision without touching anything. simulate-principal-policy evaluates the principal's attached policies, its boundary, and SCPs if the account is in an organization. Per the simulator docs it does not support RCPs, does not fetch resource policies outside the console (you pass them in), and can differ from reality for VPC endpoint policies and role chaining.

# Does this role's policy set allow GetObject on this key? Supply condition keys your policies reference.
aws iam simulate-principal-policy \
  --policy-source-arn "$ROLE_ARN" \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/reports/2026-10.csv \
  --context-entries "ContextKeyName=aws:SecureTransport,ContextKeyValues=true,ContextKeyType=boolean" \
  --query 'EvaluationResults[].{action:EvalActionName,decision:EvalDecision,org:OrganizationsDecisionDetail.AllowedByOrganizations,boundary:PermissionsBoundaryDecisionDetail.AllowedByPermissionsBoundary,missing:MissingContextValues}' \
  --output table

EvalDecision is allowed, explicitDeny or implicitDeny. AllowedByOrganizations: false means an SCP; AllowedByPermissionsBoundary: false means the boundary. For SCPs the simulator gives the decision but, for security reasons, not the matching statement. Before applying a fix, check it with IAM Access Analyzer, which also catches policies that would grant more than you meant:

# Grammar and best-practice findings for the policy you are about to attach.
aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://fix.json \
  --query 'findings[].[findingType,issueCode,findingDetails]' --output table

# Fail fast if the fix accidentally grants something it never should ($0.002 per check).
aws accessanalyzer check-access-not-granted --policy-type IDENTITY_POLICY --policy-document file://fix.json \
  --access 'actions=iam:PassRole,s3:DeleteBucket'

Step 5: Find the denied call in CloudTrail

CloudTrail records errorCode and errorMessage for denied calls, and S3 notes the enhanced context appears there too. For management events from the last 90 days, lookup-events is enough. It accepts one lookup attribute per call and is limited to two requests per second per account per Region, per the CloudTrail CLI docs, so filter with jq:

# Denied IAM calls in the last two hours (GNU date; on macOS use: date -u -v-2H +%Y-%m-%dT%H:%M:%SZ).
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventSource,AttributeValue=iam.amazonaws.com \
  --start-time "$(date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --max-items 500 --output json |
jq -r '.Events[].CloudTrailEvent | fromjson
  | select(.errorCode != null and (.errorCode | test("AccessDenied|UnauthorizedOperation")))
  | [.eventTime, .eventName, .userIdentity.arn, .errorCode, .errorMessage] | @tsv'
# 2026-10-24T09:12:03Z  CreateRole  arn:aws:sts::111122223333:assumed-role/ci-deploy/gha  AccessDenied  User: ... because no permissions boundary allows the iam:CreateRole action

S3 object reads, KMS decrypts through S3, and other data events are not in Event history. You need a trail with data events, then a query over its logs. AWS closed CloudTrail Lake to new customers on 31 May 2026 and recommends CloudWatch, so for a trail delivered to CloudWatch Logs:

# CloudWatch Logs Insights over a CloudTrail log group (name is whatever your trail uses).
# GNU date again; on macOS use: date -v-2H +%s
QUERY_ID=$(aws logs start-query \
  --log-group-name aws-cloudtrail-logs-111122223333 \
  --start-time "$(date -d '2 hours ago' +%s)" --end-time "$(date +%s)" \
  --query-string 'fields eventTime, eventSource, eventName, userIdentity.arn, errorCode, errorMessage
    | filter errorCode like /AccessDenied|UnauthorizedOperation/
    | sort eventTime desc | limit 50' \
  --query queryId --output text)
sleep 5; aws logs get-query-results --query-id "$QUERY_ID" --output json | jq -r '.results[] | map(.value) | @tsv'

One blind spot: a request denied by a VPC endpoint policy may not be logged at all when the endpoint owner account differs from the caller or target account.

Step 6: Clear SCP and RCP denials from the organization

If the message says "service control policy" or "resource control policy", no change in the account will help. SCPs cap principals in member accounts and RCPs cap resources in member accounts; neither affects the management account or service-linked roles, per the SCP and RCP pages. list-policies-for-target returns only policies attached directly, so walk up the tree:

# Run from the management account or a delegated administrator.
ACCOUNT=111122223333
TARGET=$ACCOUNT
while [ -n "$TARGET" ]; do
  echo "== $TARGET"
  for F in SERVICE_CONTROL_POLICY RESOURCE_CONTROL_POLICY; do
    aws organizations list-policies-for-target --target-id "$TARGET" --filter "$F" \
      --query 'Policies[].[Id,Name]' --output text | sed "s/^/$F  /"
  done
  # Stop after the root (r-xxxx); otherwise climb to the parent OU.
  case "$TARGET" in r-*) break ;; esac
  TARGET=$(aws organizations list-parents --child-id "$TARGET" --query 'Parents[0].Id' --output text)
done

# Read the one the error named (the p-... id at the end of the policy ARN).
aws organizations describe-policy --policy-id p-examplepolicyid123 --query Policy.Content --output text | jq .

Common culprits are Region-restriction SCPs using aws:RequestedRegion, allow-list SCPs where FullAWSAccess was replaced and a new service was never added, and RCPs restricting S3 or KMS to aws:PrincipalOrgID that now block a vendor. If the guardrail is right, the fix is an exception scoped to a role ARN with aws:PrincipalArn, tested on one OU before it moves up.

Step 7: Check permission boundaries, session policies and trust policies

A boundary or session policy denial means the role's own policies allow the action but a ceiling does not.

# Is there a boundary, and what does it allow? "None" means no boundary: skip the next two commands.
BOUNDARY_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.PermissionsBoundary.PermissionsBoundaryArn' --output text)
echo "$BOUNDARY_ARN"
VERSION=$(aws iam get-policy --policy-arn "$BOUNDARY_ARN" --query Policy.DefaultVersionId --output text)
aws iam get-policy-version --policy-arn "$BOUNDARY_ARN" --version-id "$VERSION" --query PolicyVersion.Document

# Can I assume the target role at all? Check its trust policy.
aws iam get-role --role-name deploy-target --query Role.AssumeRolePolicyDocument

Session policies are passed on AssumeRole with --policy or up to ten --policy-arns, and the session gets the intersection with the role's policies (IAM docs). A session policy is not attached to the role, so get-role will never show it; check whichever tool minted the credentials. For sts:AssumeRole denials, the role troubleshooting page lists the usual suspects: case-sensitive role names, a caller account missing from the trust policy's Principal, unmet conditions such as external ID, and trust policies that lack sts:SetSourceIdentity or sts:TagSession when the caller passes a source identity or session tags. For GitHub Actions, AssumeRoleWithWebIdentity failures usually come down to the sub or aud condition in the trust policy, which my GitHub Actions OIDC guide covers.

Step 8: Fix S3 bucket policy and KMS key policy denials

For S3, check the policies on the bucket and the encryption on the object:

BUCKET=amzn-s3-demo-bucket
aws s3api get-bucket-policy --bucket "$BUCKET" --query Policy --output text | jq .
aws s3api get-public-access-block --bucket "$BUCKET"
aws s3api get-bucket-ownership-controls --bucket "$BUCKET"
# Which key encrypts the object? A KMS key ARN here means you also need kms:Decrypt.
aws s3api head-object --bucket "$BUCKET" --key reports/2026-10.csv --query '[ServerSideEncryption,SSEKMSKeyId]'
aws kms get-key-policy --key-id <key-arn-from-above> --query Policy --output text | jq .

Three rules cover most cases. First, s3:ListBucket applies to the bucket ARN and s3:GetObject to object ARNs, and without ListBucket a missing key returns 403 instead of 404, so "access denied" sometimes means "not found". Second, SSE-KMS needs kms:GenerateDataKey to upload and kms:Decrypt to download, and the S3 guide notes that objects under the AWS managed aws/s3 key can only be read from the account that owns it. Third, for cross-account access both sides must allow: the bucket policy and key policy in the owner account, and an identity policy in the caller's account, as the KMS troubleshooting page walks through.

An identity policy for reading one prefix of a KMS-encrypted bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListOnlyTheReportsPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
      "Condition": { "StringLike": { "s3:prefix": ["reports/*"] } }
    },
    {
      "Sid": "ReadReports",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/reports/*"
    },
    {
      "Sid": "DecryptOnlyViaS3",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
      "Condition": { "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" } }
    }
  ]
}

If that policy is attached and the simulator says allowed but the real call still fails, the denial is on the resource side or in a policy the simulator does not model: the key policy, an RCP, or a VPC endpoint policy.

Troubleshooting

Symptom Likely cause Fix
Denied as a role or account you did not expect Env vars or another profile override your credentials aws sts get-caller-identity, aws configure list, unset stale AWS_* variables
because no identity-based policy allows Action or resource ARN missing from the role's policies Add a scoped Allow; validate with simulate-principal-policy
explicit deny in a service control policy Region, service or tag guardrail at OU or root Organization admin adds a scoped exception or moves the account
explicit deny in a resource control policy Org-only perimeter on S3, KMS, STS or another RCP service Admin adds an exception for the trusted principal or service
no permissions boundary allows Role's boundary lacks the action Platform team widens the boundary, not the role
Simulator says allowed, real call denied Session policy, RCP, VPC endpoint or resource policy Check how credentials were minted, then the resource side
S3 GetObject 403 on a key that may not exist No s3:ListBucket, so S3 hides the 404 Grant s3:ListBucket on the bucket ARN
S3 403 only on encrypted objects Missing kms:Decrypt or key policy does not allow you Add kms:Decrypt and check the key policy; avoid aws/s3 cross-account
not authorized to perform: sts:AssumeRole Trust policy principal, condition, or missing sts:TagSession / sts:SetSourceIdentity Fix the target role's trust policy and the caller's Allow
not authorized to perform: iam:PassRole Caller may not hand that role to the service Allow iam:PassRole on that role ARN only, ideally with iam:PassedToService

FAQ

What does "is not authorized to perform" mean in AWS?

It means AWS evaluated every policy that applied to your request and found either an explicit Deny or no Allow. The rest of the message names the principal, action and resource, and usually which policy type failed. For same-account and same-organization explicit denies it now includes the denying policy's ARN.

How do I decode an AWS encoded authorization failure message?

Run aws sts decode-authorization-message --encoded-message "<blob>" and pipe the DecodedMessage field through jq. You need the sts:DecodeAuthorizationMessage permission, and the blob must be pasted exactly, without line breaks. The output shows whether the deny was explicit and the condition values AWS evaluated.

Why does S3 return Access Denied instead of 404 Not Found?

If you lack s3:ListBucket on the bucket, S3 returns 403 for a missing object so you cannot probe which keys exist. Grant s3:ListBucket on the bucket ARN and the same request will return 404 when the key is absent.

Does the IAM policy simulator check SCPs and RCPs?

It evaluates SCPs for accounts in an organization, though it does not show the matching SCP statement. It does not support RCPs, and it can differ from live behaviour for VPC endpoint policies and role chaining. Treat an allowed result as necessary, not sufficient.

When an AccessDenied lands on my desk, the first thing I check is get-caller-identity, because the most common answer is "you were not who you thought". The second is the last clause of the message, which since January usually tells me which policy to open. I only reach for the simulator and CloudTrail when those two disagree, and I fix the narrowest policy that caused the denial rather than widening the role, because a wider role is how next year's incident starts. The official references worth bookmarking are the IAM access denied troubleshooting page, the policy evaluation logic, the S3 403 troubleshooting guide and the policy simulator docs.

Related: Workload Identity Federation Explained (2026): OIDC Instead of Cloud Keys, How to Use GitHub Actions OIDC With AWS, GCP and Azure (2026) and Secrets Sprawl 2026: 28.65M Leaked Keys and 64% Still Valid Years Later.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related