AWS

AWS: is not authorized to perform

Read the error properly. It names the principal, the action and the resource. Then work through the six things that deny a request, in the order they actually occur.

medium fix7 min read

aws. The error
An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::123456789012:assumed-role/AppRole/i-0abc123 is not
authorized to perform: s3:PutObject on resource:
arn:aws:s3:::acme-uploads/report.pdf because no identity-based policy
allows the s3:PutObject action

An error occurred (AccessDenied) when calling the AssumeRole operation:
User: arn:aws:iam::123456789012:user/ci is not authorized to perform:
sts:AssumeRole on resource: arn:aws:iam::210987654321:role/Deployer

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Confirm which identity is really being used

    aws sts get-caller-identity

    The Arn here is the principal the policy has to allow, and it is often not the one you assumed. If it is wrong, the credential chain picked something else up. Check env | grep -i AWS and aws configure list.

  2. 2

    Ask IAM why it said no, rather than guessing

    aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::123456789012:role/AppRole --action-names s3:PutObject --resource-arns arn:aws:s3:::acme-uploads/report.pdf --query 'EvaluationResults[].[EvalDecision,MatchedStatements[].SourcePolicyId]'

    implicitDeny means nothing grants the action, so add it. explicitDeny names the policy doing the blocking, and an explicit Deny beats every Allow anywhere.

  3. 3

    If the resource belongs to another account, check its own policy

    aws s3api get-bucket-policy --bucket acme-uploads --query Policy --output text | python3 -m json.tool

    Resource policies are evaluated alongside identity policies. For cross-account access both sides have to allow it, and a missing bucket policy denies you no matter how permissive your role is.

All 14 sections

AWS AccessDenied messages are more informative than they look. Before changing any policy, read the three facts the message gives you.

User: arn:aws:sts::123456789012:assumed-role/AppRole/i-0abc123
      ↑ the principal, which identity actually made the call
is not authorized to perform: s3:PutObject
      ↑ the action. The exact API operation
on resource: arn:aws:s3:::acme-uploads/report.pdf
      ↑ the resource, including the object key
because no identity-based policy allows the s3:PutObject action
      ↑ the reason. This one names the cause outright

That last clause is newer and worth looking for. AWS now often states why, and the wording distinguishes the causes below.

First: confirm who you actually are

More AccessDenied investigations are wasted on this than on anything else.

aws sts get-caller-identity
{
  "UserId": "AROAEXAMPLE:i-0abc123",
  "Account": "123456789012",
  "Arn": "arn:aws:sts::123456789012:assumed-role/AppRole/i-0abc123"
}

Check the account and the role. A surprising share of these errors are a correct policy in the wrong account, a stale AWS_PROFILE, or an EC2 instance using its instance role when you assumed you were using your own credentials.

env | grep -i aws        # environment variables override the profile
aws configure list       # shows which source each value came from

Environment variables beat the profile, so an AWS_ACCESS_KEY_ID left over from an earlier session silently overrides --profile.

Then: ask IAM directly

Rather than reasoning about the policies, simulate the call:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/AppRole \
  --action-names s3:PutObject \
  --resource-arns arn:aws:s3:::acme-uploads/report.pdf \
  --query 'EvaluationResults[].[EvalActionName,EvalDecision,MatchedStatements[].SourcePolicyId]'
[["s3:PutObject", "implicitDeny", null]]

implicitDeny means nothing allows it. explicitDeny means something actively forbids it, and MatchedStatements names the policy, which is the fastest route to a deny buried in an SCP or a boundary.

Note the simulator does not evaluate resource policies or SCPs in every case, so a pass here with a real failure points at one of those.

The six causes, in order of likelihood

1. No identity-based policy allows it

The default state: IAM denies everything not explicitly allowed. The message says because no identity-based policy allows.

aws iam list-attached-role-policies --role-name AppRole
aws iam list-role-policies --role-name AppRole

Add the action, scoped to the resource:

{
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:PutObjectAcl"],
  "Resource": "arn:aws:s3:::acme-uploads/*"
}

Watch the ARN shape. Bucket-level and object-level actions take different resources: s3:ListBucket needs arn:aws:s3:::acme-uploads, while s3:PutObject needs arn:aws:s3:::acme-uploads/*. Using one where the other belongs is the single most common S3 policy mistake.

2. A resource policy denies or does not allow it

For cross-account access, both sides must permit the call. A bucket policy, KMS key policy, SQS queue policy or Lambda resource policy can allow or deny independently of IAM.

aws s3api get-bucket-policy --bucket acme-uploads \
  --query Policy --output text | python3 -m json.tool

The special case worth knowing: KMS requires both the key policy and IAM, and an IAM policy granting kms:* achieves nothing on a key whose policy does not permit your principal. A PutObject to an SSE-KMS bucket therefore fails with an S3-shaped error caused by KMS.

3. An explicit deny

A single Deny anywhere beats every Allow. The message reads because an explicit deny in an identity-based policy.

aws iam get-role-policy --role-name AppRole --policy-name inline \
  --query 'PolicyDocument.Statement[?Effect==`Deny`]'

Common sources: a policy denying actions outside a region, denying access unless MFA is present, or denying anything not carrying a required tag.

4. A service control policy

... with an explicit deny in a service control policy

That phrase is the giveaway, and it appears at the end of a long message where it is easy to miss. An SCP caps what is possible in the account regardless of IAM.

aws organizations list-policies-for-target --target-id 123456789012 \
  --filter SERVICE_CONTROL_POLICY

You need Organizations access to check, which usually means asking whoever manages the management account. Region-restriction SCPs are a frequent cause, and they break global services unless exempted.

5. A permissions boundary

A boundary caps what a role can do even when its policies allow more. The message says because no permissions boundary allows.

aws iam get-role --role-name AppRole --query 'Role.PermissionsBoundary'

6. A session policy

When a role is assumed with --policy or --policy-arns, that session is limited further:

aws sts assume-role --role-arn "$ROLE" --role-session-name test \
  --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'

The resulting credentials cannot write to S3 no matter what the role permits. Federated sessions from an identity provider frequently carry one of these.

Reading which cause applies

The because … clause maps directly:

ClauseCause
no identity-based policy allowsMissing IAM permission
explicit deny in an identity-based policyA Deny in your IAM policy
explicit deny in a service control policyAn SCP
no permissions boundary allowsA permissions boundary
explicit deny in a resource-based policyBucket, key or queue policy
no session policy allowsSession policy on the assumed role

If your error has no because clause, the API is an older one. Use the simulator.

Cross-account AssumeRole

User: arn:aws:iam::123456789012:user/ci is not authorized to perform:
sts:AssumeRole on resource: arn:aws:iam::210987654321:role/Deployer

Two separate permissions, and both are needed:

The caller needs sts:AssumeRole on that role ARN:

{ "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::210987654321:role/Deployer" }

The role needs a trust policy accepting the caller:

aws iam get-role --role-name Deployer --query 'Role.AssumeRolePolicyDocument'
{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::123456789012:user/ci" },
  "Action": "sts:AssumeRole",
  "Condition": { "StringEquals": { "sts:ExternalId": "acme-ci" } }
}

Missing either gives an identical error. And note the ExternalId condition, if the trust policy requires one, the caller must pass --external-id or the assume fails with the same message.

Find it in CloudTrail

Every denied call is recorded, with the full request context:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
  --start-time "$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)" \
  --query 'Events[?contains(CloudTrailEvent, `AccessDenied`)].CloudTrailEvent' \
  --output text | python3 -m json.tool | head -40

The event includes userIdentity, requestParameters and errorMessage, which together settle arguments about what was actually requested, often revealing the call used a different resource ARN than anyone assumed.

A checklist

  1. Read the principal, action and resource from the message.
  2. aws sts get-caller-identity. Right account, right role?
  3. env | grep -i aws. Is something overriding your profile?
  4. Read the because … clause; it names the cause.
  5. aws iam simulate-principal-policy. Implicit or explicit deny?
  6. Check bucket-level vs object-level ARN shape for S3.
  7. SSE-KMS bucket → check the key policy, not just IAM.
  8. Cross-account → caller permission and trust policy, plus any ExternalId.
  9. Still unclear → CloudTrail has the full request.

Frequently Asked Questions

How do I read an AWS "is not authorized to perform" error?

It gives you four things: the principal ARN that made the call, the exact API action, the resource ARN including any object key, and increasingly a because … clause naming the cause. Start with the principal. Confirm with aws sts get-caller-identity that you are the identity you think you are, in the account you think you are. A large share of these errors turn out to be a correct policy in the wrong account or a stale environment variable overriding your profile.

Why does my IAM policy allow the action but the call still fails?

Six things can deny a request, and IAM is only one. A resource policy, bucket, KMS key, queue, can refuse independently. An explicit Deny anywhere overrides every Allow. A service control policy caps what is possible in the whole account. A permissions boundary caps what the role can do. And a session policy can restrict credentials further when a role is assumed with --policy. The because … clause in the error tells you which applies.

Why do I get AccessDenied on S3 when my policy looks right?

Most often the resource ARN shape. Bucket-level actions such as s3:ListBucket require arn:aws:s3:::bucket, while object-level actions such as s3:GetObject and s3:PutObject require arn:aws:s3:::bucket/*, and using one form where the other belongs denies the call with a policy that reads as correct. The second cause is an SSE-KMS bucket: writing to it needs kms:GenerateDataKey on the key, permitted by both the key policy and IAM.

What does "with an explicit deny in a service control policy" mean?

An SCP attached to your account or one of its parent organizational units forbids that action, and no IAM policy can override it. SCPs set a ceiling on what is possible. The phrase appears at the end of a long message where it is easy to overlook, and it is the only reliable indicator, since an implicit denial under an allow-list SCP gives no explanation at all. Checking requires Organizations access, so it usually means asking whoever manages the management account.

Why does sts:AssumeRole fail across accounts?

Because it needs permission on both sides. The calling identity requires sts:AssumeRole on the target role's ARN in its own IAM policy, and the target role requires a trust policy naming that caller as a principal. Missing either produces an identical error. If the trust policy includes an sts:ExternalId condition, the caller must also pass --external-id with the matching value, which fails the same way when omitted.

How do I find out exactly why a request was denied?

aws iam simulate-principal-policy with the action and resource ARN returns implicitDeny or explicitDeny, and for an explicit deny names the policy in MatchedStatements, which is much faster than reading policies by hand. It does not evaluate every resource policy or SCP, so a pass there with a real-world failure points at one of those. CloudTrail holds the complete request for any denied call, including the resource ARN actually used, which frequently differs from what everyone assumed.

Learn the underlying concept