S3 checks several policy layers. An explicit Deny anywhere wins, and the request needs an Allow from the caller's side and (for cross-account access) the bucket's side.
1. Identify the caller and the request
aws sts get-caller-identity
aws s3api head-object --bucket prod-data --key reports/2026/10/report.csv --debug 2>&1 | grep -iE "x-amz-request-id|x-amz-id-2|403"
Recent S3 403 messages say which policy type denied the call ("...because no identity-based policy allows..." or "...explicit deny in a resource-based policy"). Keep the request ID for CloudTrail or AWS Support.
2. Checklist
- Identity policy allows the action on the right ARN: bucket-level actions (
s3:ListBucket) usearn:aws:s3:::bucket, object actions (s3:GetObject) usearn:aws:s3:::bucket/*. - Bucket policy has no explicit
Deny. Look for conditions such asaws:SourceVpce,aws:SourceIp,aws:PrincipalOrgIDandaws:SecureTransport.aws s3api get-bucket-policy --bucket prod-data --query Policy --output text | python3 -m json.tool - Block Public Access (account and bucket) is the reason anonymous or public reads fail. That's by design.
- Object ownership: with
BucketOwnerEnforced(the default for new buckets) ACLs are disabled, so grant access with policies, not ACLs. On older buckets, objects uploaded by another account may be owned by the uploader, and the bucket owner can't read them.aws s3api get-bucket-ownership-controls --bucket prod-data - KMS: objects encrypted with SSE-KMS need
kms:Decrypt(to read) andkms:GenerateDataKey(to write) in both the IAM policy and the key policy. A cross-account caller can't use the AWS-managedaws/s3key at all, so use a customer-managed key.aws s3api head-object --bucket prod-data --key reports/2026/10/report.csv --query "{sse:ServerSideEncryption,key:SSEKMSKeyId}" - VPC endpoint policy: callers inside a VPC that use a gateway endpoint are limited by that endpoint's policy.
- SCPs / RCPs in AWS Organizations deny the action or region.
- Permissions boundary or session policy on the role.
- The object exists. Without
s3:ListBucket, S3 returns 403 instead of 404 for missing keys.
3. Find it in CloudTrail
Object-level calls (GetObject, PutObject) are data events. They're only logged if data events are enabled for the bucket. Query them in CloudTrail Lake or Athena, filtering on errorCode = 'AccessDenied', and compare the userIdentity.arn with what you expected.
4. Use IAM Access Analyzer
- Policy validation catches ARN and syntax mistakes.
- External access findings show who outside the account can reach the bucket.
Tip
When a customer reports "the file opens in the console but not in the app", the console user and the app role are different principals. Compare their policies, and check KMS.