What the Error Actually Means
AWS S3 access errors have gotten complicated with all the conflicting advice flying around. I spent a frustrating Tuesday afternoon convinced my bucket policy was broken — turns out the policy was fine. The real culprit was something sitting silently upstream, overriding everything I’d written.
Here’s the thing about Access Denied, 403 Forbidden, and AllAccessDisabled: they look identical on the surface. Same error. Four completely different root causes. Your bucket policy could be perfectly written. Your IAM role could have explicit permissions. You’ll still get locked out if Block Public Access is enabled or a deny statement exists somewhere higher up the chain. The trick is ruling each cause out systematically — not rewriting the policy blindly and hoping something sticks.
Check the Bucket Policy First
Start here. It’s the most obvious place, and you need to actually see what you’re working with before you touch anything.
Open the AWS Management Console, navigate to S3, select your bucket, and click the Permissions tab. Scroll to Bucket Policy and hit Edit. You’ll see the raw JSON. If it’s empty, that’s already a problem — a missing policy is behind more access failures than people realize.
Prefer the command line? Run this:
aws s3api get-bucket-policy --bucket my-bucket-name
Here’s what broken looks like. This policy is missing its Principal field entirely:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket-name/*"
}
]
}
No Principal means the policy applies to nobody. Zero people. Here’s the corrected version:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket-name/*"
}
]
}
That Principal field is mandatory. The wildcard "*" means any authenticated AWS identity. Need something more specific? Swap it for a full ARN:
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyRole"
}
The other trap worth mentioning — accidentally writing "Effect": "Deny" when you meant Allow. I did this once with a policy copied straight from AWS documentation. A single Deny statement overrides every Allow in the entire account. Don’t make my mistake.
Block Public Access Settings Override Your Policy
Probably should have opened with this section, honestly.
Even a flawless bucket policy can fail here. AWS built a second gate called Block Public Access — four toggles that sit above your bucket policy and silently override it. No error message explains that’s what’s happening. You just stay locked out.
In the console, go to your bucket’s Permissions tab and find the Block Public Access (bucket settings) section. Four toggles:
- Block all public access — The nuclear option. Flips all four on simultaneously.
- Block public access to ACLs — Stops public ACL modifications cold.
- Ignore public ACLs — Treats all ACLs as private regardless of what they actually say.
- Block public access to bucket policy — Prevents bucket policies from granting public access.
- Ignore bucket policies that grant public access — Quietly ignores public policies without telling you a thing.
Any of these enabled while your policy has a wildcard Principal and access will simply not happen.
Check current state via CLI:
aws s3api get-public-access-block --bucket my-bucket-name
You’ll get four boolean flags back. If "BlockPublicPolicy" or "IgnorePublicAcls" are showing true and you genuinely need public access, disable them:
aws s3api put-public-access-block --bucket my-bucket-name \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"
That’s for truly public buckets. Most production setups should keep these enabled and route access through specific IAM roles — not wildcards.
IAM Role and User Permissions That Conflict
Bucket policy says allow. IAM role says deny. The deny wins. Always, without exception.
Cross-account setups are where this gets painful. You’ve configured Account B to grant access to a role in Account A. The bucket policy in B looks correct. Trust relationship in A is set up. Still can’t get in. I’ve been here — it cost me about three hours on a Wednesday I’d rather forget.
The problem is usually a missing statement in the role’s permissions policy. A trust relationship lets Account A assume the role, sure. But that role still needs explicit permission to actually call S3 actions. Two separate things.
Cross-account example. In Account B, the bucket policy grants:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/CrossAccountRole"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket-name/*"
}
In Account A, CrossAccountRole needs this attached or inline:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket-name/*"
}
]
}
Without both pieces in place, access fails. No partial credit.
Test before you deploy — at least if you want to avoid debugging this at 11pm. Open the IAM console, click Policy Simulator in the left nav, select your user or role, choose the S3 service, pick GetObject, and drop in your bucket ARN. It tells you exactly whether that identity can perform the action or not.
Quick Checklist Before You Give Up
- Bucket policy syntax — Run it through a JSON validator. Missing commas and mismatched braces hide surprisingly well.
- Principal field exists — Every Allow statement needs one. Wildcards, ARNs, or account IDs all work.
- Block Public Access settings — All four toggles. Make sure they match your actual intent. Use the CLI command above if the console is being slow.
- IAM role trust policy — Cross-account setup? Verify the role can actually be assumed by the other account. Check the trust relationship tab in IAM directly.
- IAM role permissions — The role needs its own S3 permissions policy, completely separate from the bucket policy. Both must allow the action.
- Cross-account principal format — Full ARN only:
arn:aws:iam::ACCOUNT-ID:role/RoleName. A wrong account ID or missing colon fails silently. - VPC endpoint policy — Accessing S3 through a VPC endpoint? That endpoint has its own policy. Another gate entirely.
- CloudTrail for forensics — Enable CloudTrail, attempt the access, check the logs. You’ll see the exact principal that made the call and the precise denial reason.
When you’re truly stuck — and sometimes you will be — the AWS Policy Simulator lets you test your exact setup end-to-end. It’s saved me more than once.
Stay in the loop
Get the latest team aws updates delivered to your inbox.