I set up an IAM policy granting a user full S3 access (s3:* on all resources), but when they tried to access the bucket, they got an AccessDenied error. The bucket policy seemed fine—it allowed public access—but something wasn’t adding up. I spent hours reviewing both policies, only to realize the interaction between IAM and bucket policies was exactly the opposite of what I thought. In this post, I’ll walk through exactly what causes this and how to fix it.
The Problem
Your IAM policy allows s3:* on all resources, but bucket operations still fail with AccessDenied. Or a bucket policy explicitly allows access, but IAM policies deny it. The two policies seem to be fighting each other, and you’re not sure which one takes precedence.
| Error Type | Description |
|---|---|
| AccessDenied with IAM Allow | IAM allows action, but bucket policy (or account setting) denies it. |
| AccessDenied with Bucket Policy Allow | Bucket policy allows, but IAM policy denies or doesn’t grant. |
| Cross-Account Denied | Both policies allow, but the interaction across accounts is wrong. |
Why Does This Happen?
The interaction between IAM and bucket policies depends on whether the access is same-account or cross-account:
- Same-account access — Both the IAM policy and bucket policy are evaluated with an OR logic (union). Either one can grant access. An explicit Deny in either one blocks access.
- Cross-account access — Both policies must allow. An explicit Allow in IAM AND an explicit Allow in the bucket policy are required. One Allow is not enough.
- Explicit Deny always blocks — An explicit Deny statement in any policy (IAM or bucket) overrides any Allow statement. Denies are absolute.
- Bucket policy with Principal: “*“ — Using
"Principal": "*"without conditions is extremely permissive but can conflict with IAM policies in unexpected ways.
The Fix
For Same-Account Access
In same-account scenarios, ensure the IAM policy grants the action, and the bucket policy doesn’t explicitly deny it:
# IAM policy allowing S3 access
cat > iam-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
EOF
aws iam put-user-policy \
--user-name my-user \
--policy-name s3-access \
--policy-document file://iam-policy.json
For the bucket policy, you can either:
- Not have a bucket policy (IAM is sufficient)
- Have a bucket policy that allows the same actions
- Have a bucket policy that allows a broader set of actions
For Cross-Account Access
For cross-account access, BOTH policies must Allow:
# In the resource account (where the bucket is)
# Bucket policy allowing the external account
cat > bucket-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::EXTERNAL_ACCOUNT_ID:role/external-role"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
EOF
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy file://bucket-policy.json
# In the external account
# IAM policy allowing the user/role to access the bucket
cat > external-iam-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
EOF
aws iam put-role-policy \
--role-name external-role \
--policy-name s3-cross-account \
--policy-document file://external-iam-policy.json
Using AWS Policy Simulator
Test the access before deploying to production:
# Simulate same-account access
aws iam simulate-custom-policy \
--policy-input-list file://iam-policy.json \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/myfile.txt
# Simulate principal-based access
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:user/my-user \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::my-bucket/myfile.txt
Resolving Explicit Deny Conflicts
If you have an explicit Deny blocking access:
# List all bucket policies
aws s3api get-bucket-policy --bucket my-bucket
# Look for any Statement with "Effect": "Deny"
# Remove or modify those statements
# Then reapply the bucket policy without the Deny
How to Run This
- For same-account: ensure IAM policy allows, bucket policy is either missing or allows
- For cross-account: ensure BOTH IAM (in external account) and bucket policy (in resource account) allow
- Test with Policy Simulator before applying to production
- Replace
EXTERNAL_ACCOUNT_IDwith the actual external AWS account ID - Replace
my-userandexternal-rolewith actual resource names
Is This Safe?
IAM and bucket policies can safely coexist when configured correctly. For production, follow the principle of least privilege: grant only the minimum permissions needed. Avoid s3:* on *—instead, specify exact actions and resources.
Key Takeaway
Same-account access uses OR logic (either policy can allow). Cross-account access uses AND logic (both must allow). Explicit Deny always blocks. Test with Policy Simulator to avoid deployment surprises.
Have questions or ran into a different S3 issue? Connect with me on LinkedIn or X.