AWS S3 403 Access Denied: The 3 Real Fixes That Work
S3 403 errors almost always come down to three things: bucket policy, IAM permissions, or block public access. Here's how to fix each one, fast.
1. Bucket Policy Blocking Access (The Most Common Culprit)
Had a client last month whose entire dev team couldn't upload to an S3 bucket. Everyone got 403s. Turned out someone attached a bucket policy that said Deny for any s3:PutObject action from IPs outside the office. Only problem? They were all working remote that day.
The bucket policy is evaluated before IAM permissions. If it says Deny, nothing else matters. Here's what to check:
- Go to the S3 bucket in the AWS console.
- Click the Permissions tab.
- Scroll to Bucket Policy and click Edit.
- Look for any
Denystatements. They look like this:
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "192.0.2.0/24"
}
}
}
If you see a Deny with a condition, that's likely your blocker. Either remove the Deny statement entirely, or adjust the condition. Common conditions that bite people: IpAddress (only allows specific IPs), StringNotEquals (denies all but one VPC), or Bool with aws:SecureTransport set to false (blocks HTTP).
Quick test: temporarily delete the bucket policy. If the 403 goes away, the policy was the problem. Then rebuild it carefully. I always test policies with a single user first before rolling to a team.
2. IAM Permissions Missing or Too Narrow
Second most common cause: the IAM user or role doesn't have the right permissions. I see this all the time with new devs who copy a policy from a blog post but miss a key action.
You need both s3:GetObject (to read) and s3:PutObject (to write) at minimum. But here's the gotcha — you also need s3:ListBucket on the bucket itself if you want to see the objects in the console. Without it, you get 403s on the list call even if GetObject works.
Here's a minimal IAM policy that actually works for read/write access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket"
}
]
}
Real scenario: Had a client who couldn't upload files via their web app. They had s3:PutObject on the bucket. But the app was using a presigned URL generated by a Lambda role that only had s3:PutObject — no s3:GetObject. The presigned URL generation failed with a 403. Added s3:GetObject to the Lambda role, and it worked instantly.
Check the IAM policy attached to the user, role, or service that's hitting S3. You can use the Policy Simulator in the IAM console to test exactly which actions are allowed. I use it every time I write a new policy — saves hours of trial and error.
3. Block Public Access Settings (For Public Buckets)
If you're trying to make a bucket public — for static website hosting, for example — and you're still getting 403s, check the Block Public Access settings. AWS added these in 2018 to prevent accidental public exposure, and they override everything else.
Go to the bucket's Permissions tab. Look for Block public access (bucket settings). If any of the four checkboxes are checked, they'll block public access no matter what your bucket policy says.
For a truly public bucket, you need to:
- Uncheck all four boxes.
- Click Save changes.
- Then apply a bucket policy that allows
s3:GetObjectfromPrincipal: "*".
Here's that policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-public-bucket/*"
}
]
}
Important: AWS account-level block public access settings also apply. If your AWS account has block public access enabled at the account level (check in the S3 console under Block Public Access on the left sidebar), you can't override it for any bucket in that account. I ran into this at a company that had a strict security policy — had to get an exception approved before their static site would serve.
One more thing: if you're using presigned URLs to give temporary access to private objects, Block Public Access isn't the issue — presigned URLs bypass it. So if you're seeing 403s with presigned URLs, go back to causes 1 and 2.
Quick-Reference Summary Table
| Cause | Check This First | Fix |
|---|---|---|
| Bucket Policy Deny | Permissions tab > Bucket Policy | Remove or adjust Deny statement |
| Missing IAM Permissions | IAM user/role policy | Add s3:GetObject, s3:PutObject, s3:ListBucket |
| Block Public Access | Permissions tab > Block public access | Uncheck boxes (for public buckets) |
That's the three things I check every single time someone calls me with an S3 403. Fix these, and you're back in business.
Was this solution helpful?