AWS combines several policy layers when it evaluates a request. An explicit Deny in any layer wins, and if no layer allows the call, it's denied. Go through the layers in this order.
1. Who is actually calling?
aws sts get-caller-identity
Many "but I have admin" tickets are the wrong profile, an expired SSO session, or a role assumed through a chain the user forgot about. In pods and Lambda functions, check the identity of the runtime (IRSA/Pod Identity role, execution role), not your own.
2. Read the full error
Newer error messages name the policy type that denied the call:
User: arn:aws:sts::111122223333:assumed-role/app-role/i-0abc is not authorized to perform:
s3:PutObject on resource: arn:aws:s3:::prod-bucket/x because no identity-based policy allows the s3:PutObject action
| Phrase in error | Layer to look at |
|---|---|
no identity-based policy allows | Policies on the user or role |
explicit deny in a service control policy | Organizations SCP |
explicit deny in a resource-based policy | Bucket, KMS key, queue or other resource policy |
permissions boundary | The boundary attached to the role |
session policy | Policy passed during AssumeRole |
EC2 and some other services return an encoded message instead. Decode it:
aws sts decode-authorization-message --encoded-message "<blob>" \
--query DecodedMessage --output text | python3 -m json.tool
The decoded message shows the exact action, resource and context keys, and whether the deny was explicit.
3. Find the call in CloudTrail
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
--start-time "$(date -u -d '-30 min' +%FT%TZ)" --max-results 20 \
--query "Events[].CloudTrailEvent" --output text | jq -r 'select(.errorCode!=null) | [.eventTime,.userIdentity.arn,.errorCode,.errorMessage] | @tsv'
Note
lookup-eventsonly returns management events. S3 object-level and Lambda invoke calls need data events, which you query in CloudTrail Lake or Athena.
4. Simulate the policy
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/app-role \
--action-names s3:PutObject kms:GenerateDataKey \
--resource-arns arn:aws:s3:::prod-bucket/x \
--query "EvaluationResults[].{action:EvalActionName,decision:EvalDecision,matched:MatchedStatements[0].SourcePolicyId}" --output table
The simulator only checks identity policies and boundaries. It doesn't evaluate SCPs or most resource policies, so if it says allowed but the real call fails, look at those next.
5. Usual causes
- KMS: the S3 call is allowed, but the role can't use the bucket's KMS key (
kms:GenerateDataKey,kms:Decrypt). Check the key policy. - Conditions:
aws:SourceVpce,aws:PrincipalOrgID,aws:RequestedRegionor MFA conditions that fail silently. - Cross-account access: both sides must allow it. That means the identity policy in account A and the resource policy in account B.
- Wildcard ARNs:
arn:aws:s3:::bucketmatches the bucket only. Object actions needarn:aws:s3:::bucket/*.
Fix it without over-granting
Use IAM Access Analyzer's policy generation from CloudTrail activity to build a least-privilege policy, instead of attaching *:* to get past the error.