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:

  1. Not have a bucket policy (IAM is sufficient)
  2. Have a bucket policy that allows the same actions
  3. 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

  1. For same-account: ensure IAM policy allows, bucket policy is either missing or allows
  2. For cross-account: ensure BOTH IAM (in external account) and bucket policy (in resource account) allow
  3. Test with Policy Simulator before applying to production
  4. Replace EXTERNAL_ACCOUNT_ID with the actual external AWS account ID
  5. Replace my-user and external-role with 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.