AccessDenied

S3 Bucket Policy Says Access Denied? The One-Line Fix

Server & Cloud Intermediate 👁 11 views 📅 Jun 14, 2026

Your S3 bucket policy probably has an explicit deny or a missing principal. Here's exactly what to check and the one-line fix.

You Wrote a Bucket Policy, But Still Get AccessDenied

You're not alone. It's infuriating: you craft what looks like a perfect S3 bucket policy—allowing s3:GetObject for your IAM user or role—and S3 still slaps back with AccessDenied. Let me show you the one-line fix and why your brain is working against you.

The Fix: Remove the Implicit Deny First

Here's the real problem: most people add an allow statement but forget that S3 bucket policies have an implicit deny at the end. If you don't explicitly allow the action for the correct Principal, S3 denies it. Period.

Check your bucket policy's Principal block. If you're using "Principal": "*", that works, but only if nothing else in the policy denies it. The one-line fix is usually this:

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/YOUR_USER"
  },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::YOUR_BUCKET/*"
}

Replace YOUR_ACCOUNT_ID and YOUR_USER with actual values. This tells S3: "Allow this specific user to read objects." If you're using a role, use the role ARN, not the user ARN.

Why Your Policy Wasn't Working

What's actually happening here is that S3 evaluates bucket policies differently than IAM policies. Bucket policies are resource-based policies attached directly to the bucket. They have two key traits:

  1. Explicit denies always win. If you have "Effect": "Deny" anywhere for that action, it overrides any allow—even if the allow comes later in the same policy.
  2. Missing Principal means nobody. If the Principal doesn't match the requester, the allow never applies, and the implicit deny kicks in.

Most people get tripped up by Principal. You might write "Principal": "*" when you mean "Principal": { "AWS": "arn:aws:iam::123456789012:root" }—the latter grants access to everyone in your account, not just the bucket owner. But the former (asterisk) includes anonymous users. If you want only your IAM user, you must specify their ARN.

Less Common Variations of the Same Issue

Here are the sneaky variants I've seen in production:

1. Condition Key Mismatch

You added a condition like "Condition": { "IpAddress": { "aws:SourceIp": "203.0.113.0/24" } }. If the request comes from outside that IP range, the allow won't match, and you get AccessDenied. Fix: test without the condition first, then add it back once the base policy works.

2. Bucket Policy Overrides IAM Permissions

You have IAM permissions that allow s3:GetObject globally, but your bucket policy has an explicit deny for all principals. That explicit deny wins. The reason step 3 works is that IAM and bucket policies are evaluated together, but an explicit deny from either source blocks the request.

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::YOUR_BUCKET/*"
}

This blocks everyone, including your admin user. If you're using it as a safety net, you need a separate allow statement that exempts specific principals via a NotPrincipal or a condition.

3. Cross-Account Access Without Proper Principal

When granting access to another AWS account, you must set "Principal": { "AWS": "arn:aws:iam::OTHER_ACCOUNT_ID:root" }. Using "AWS": "*" won't work for cross-account unless you also set "Condition": { "StringEquals": { "aws:SourceAccount": "OTHER_ACCOUNT_ID" } }. I've seen people leave this out and spend hours debugging.

4. Policy Size Limit

Bucket policies have a 20 KB limit. If your policy exceeds that, S3 silently ignores it or returns AccessDenied for some actions. Check the policy size in the AWS Console under the bucket's Permissions tab.

Prevention: A Simple Check Before You Deploy

Here's what I do to avoid this headache:

  • Use the AWS Policy Simulator. Paste your bucket policy into the IAM Policy Simulator (select S3 as the service). Run a test with your user's ARN. It'll tell you exactly which statement allows or denies the action.
  • Start with a minimal policy. Include only one allow statement with "Principal": "*" and one action. Once that works, add conditions and other principals. This isolates the problem.
  • Audit with aws s3api get-bucket-policy. Run aws s3api get-bucket-policy --bucket YOUR_BUCKET to dump the raw policy. Look for any "Effect": "Deny" entries that might be catching your requests.
  • Use S3 Access Logs. Enable server access logging on the bucket. The logs show the exact reason for AccessDenied (e.g., AuthorizationHeaderMalformed or InvalidPolicyDocument).

The AWS documentation on bucket policy evaluation logic is actually good—read it once and bookmark it. You'll thank yourself the next time a policy breaks at 2 AM.

Was this solution helpful?