Amazon S3 403 AccessDenied (Static Website Hosting)
S3 returned a 403 AccessDenied XML body for a static site because the request lacked permission to read the object.
What you see
<Error> <Code>AccessDenied</Code> <Message>Access Denied</Message> <RequestId>8X2A...</RequestId> </Error>
What’s actually happening
Hitting the S3 website endpoint (or a CloudFront distribution in front of it) returns a raw XML AccessDenied page instead of your index.html. It typically affects every object, not one — the bucket isn't serving anything publicly. A common variant: the site works through the REST endpoint but not the website endpoint, or it 403s for anonymous users while you can see files fine in the console (because you're authenticated). The RequestId in the body is what AWS Support needs.
Common causes
- Block Public Access is still enabled at the bucket or account level, which overrides any 'allow public' bucket policy
- The bucket policy is missing a statement granting s3:GetObject to Principal '*' on arn:aws:s3:::bucket/*
- Object ownership / ACL mismatch — files were uploaded by a different account and the bucket owner can't read them (common with cross-account writes and ACLs disabled)
- The request hits the REST API endpoint (bucket.s3.amazonaws.com) rather than the website endpoint (bucket.s3-website-region.amazonaws.com), which behaves differently for missing objects and index docs
- CloudFront's origin access setup is wrong — using an OAC/OAI but the bucket policy doesn't grant that principal, or pointing CloudFront at the REST endpoint when it should use the website endpoint
How to fix it
- Turn off Block Public Access for a truly public siteIn the S3 console → bucket → Permissions → Block public access, uncheck the settings (or the relevant ones) and confirm. Check the account-level setting too, under the S3 console's account settings — an account-wide block silently overrides per-bucket policy. If the site is meant to be served only via CloudFront, leave BPA on and use OAC instead (see below).
- Add a public s3:GetObject bucket policyAttach a policy allowing the read. The resource must end in /* (the objects), not just the bucket ARN: {"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::YOUR-BUCKET/*"} A policy that targets arn:aws:s3:::YOUR-BUCKET (no /*) grants nothing on the objects and still 403s.
- Read the exact denial reason in CloudTrailDon't guess. CloudTrail logs each denied GetObject with an errorCode of AccessDenied and often the specific reason — BPA, missing policy, or ownership. In the CloudTrail console, filter event name GetObject, or query with Athena. This tells you which of the four usual causes you actually have instead of changing settings blindly.
- Fix object ownership / disable ACLsIf objects were written cross-account, the bucket owner may not own them. Set Bucket owner enforced (Object Ownership) to disable ACLs so the bucket owner controls everything, then re-upload or run a copy-in-place to reset ownership: aws s3 cp s3://bucket/ s3://bucket/ --recursive --metadata-directive REPLACE.
- Use the website endpoint and correct CloudFront originFor static hosting with index/error documents, point at the website endpoint (bucket.s3-website-<region>.amazonaws.com), not the REST endpoint — the REST endpoint won't apply your index document and returns AccessDenied for a key it can't find. If you front it with CloudFront and keep the bucket private, configure Origin Access Control and add the matching cloudfront.amazonaws.com service principal to the bucket policy.
Stop it recurring
Decide upfront whether the bucket is public (BPA off + GetObject policy) or private-behind-CloudFront (BPA on + OAC) and don't mix the two, since half-applied settings produce exactly this 403.