Short answer: an S3 AccessDenied is the result of several policies evaluated together. A correct bucket policy is only one of them. An explicit Deny anywhere wins, and an Allow is needed on every layer that applies.
The error
terminal
An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
How S3 decides
For a request to succeed, every layer that applies must allow it and none may deny it: identity policy, bucket policy, SCPs and RCPs from AWS Organizations, permissions boundaries, session policies, VPC endpoint policies and, for encrypted objects, the KMS key policy.
Symptom → likely cause
| What you see | Check first |
|---|---|
| Works for the bucket owner, fails for another account | Bucket policy principal, object ownership, KMS key policy |
| Works from your laptop, fails from a private subnet | VPC endpoint policy, aws:SourceVpce conditions |
| Fails only for objects encrypted with a customer managed key | KMS key policy and kms:Decrypt on the role |
| Fails for every principal in an account | SCP or RCP in AWS Organizations |
| Public access fails even with a public policy | S3 Block Public Access (account or bucket) |
8 places to check, in order
- Who is calling. Run
aws sts get-caller-identity. Many “correct” policies allow the wrong role. - Explicit Deny in the bucket policy. Look for
Denystatements with conditions such asaws:SecureTransport,aws:SourceVpceoraws:PrincipalOrgID. - Identity policy. The role needs
s3:GetObjectonarn:aws:s3:::bucket/*(objects), not only on the bucket ARN. - KMS key policy. For SSE-KMS objects, the caller needs
kms:Decryptand the key policy must allow it. - Organizations SCPs and RCPs. A deny at the organization level applies even to administrators in member accounts.
- Permissions boundary or session policy. They cap what the role can do, even if its policy allows more.
- VPC endpoint policy. A restrictive endpoint policy blocks requests that would otherwise pass.
- Block Public Access and object ownership. Public policies are ignored when Block Public Access is on; with ACLs enabled, objects written by another account may not be owned by the bucket owner.
diagnose-s3-403.sh
aws sts get-caller-identity
aws s3api get-bucket-policy --bucket my-bucket --query Policy --output text | jq .
aws s3api get-public-access-block --bucket my-bucket
aws s3api get-bucket-encryption --bucket my-bucket
aws kms get-key-policy --key-id <key-id> --policy-name default --output text | jq .
A bucket policy Deny wins over any IAM Allow. Read the Deny statements before you change roles.
Prevent it
- Use IAM Access Analyzer policy validation in CI for bucket and key policies.
- Keep bucket policies, key policies and endpoint policies in the same Terraform module, reviewed together.
- Log data events for sensitive buckets in CloudTrail, so a denial shows which policy type denied it.