S3 returns 403 — bucket policy or public access block
All client types get 403 Forbidden on a bucket
Focused incident mode — strips away everything but this runbook.
Symptoms
- All client types get 403 Forbidden on a bucket
- Access worked until an account/config change
- Website/static assets suddenly break with Access Denied
Bucket policies and Public Access Blocks are separate features that love to fight: BlockPublicPolicy makes a public bucket policy fail at PUT time, while a policy without it 403s every read. Audit both before blaming IAM.
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Bucket policy
aws s3api get-bucket-policy --bucket <BUCKET>Public access block
aws s3api get-public-access-block --bucket <BUCKET>
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- Policy references a source/VPC or condition the caller fails
- Recent policy push precedes the 403
Confirm before fixing
aws s3api get-bucket-policy --bucket <BUCKET> --query Policy --output text | grep -A2 -B2 '<CALLER-ARN>'Find the caller in the policy
Fix
aws s3api put-bucket-policy --bucket <BUCKET> --policy file://corrected-policy.jsonFix the policy (keep an explicit Deny ONLY if intentional)
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
aws sts get-caller-identityKnow the caller
aws s3api get-bucket-policy --bucket <BUCKET>Audit the policy
aws s3api get-public-access-block --bucket <BUCKET>Audit the block
aws s3api put-bucket-policy --bucket <BUCKET> --policy file://corrected-policy.jsonApply the corrected policy
aws s3 ls s3://<BUCKET>/Verify the fix
Expected: A listing with no AccessDenied
Prevention
- Apply public access block at the account level; use explicit exceptions sparingly.
- Lint bucket policies with IAM Access Analyzer before applying.
- Test public access with an anonymous role in CI after every policy change.
Related runbooks
Same stack, same failure family — worth a skim.
A role in account A cannot read a bucket owned by account B
aws s3 ls s3://<BUCKET>/ fails with AccessDenied on the instance
Freshly created role fails with AccessDenied/not authorized
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.