1. Introduction

An S3 403 Access Denied is deceptively simple: the request reached S3, S3 understood it, and S3 refused it. The hard part is that "denied" can originate from at least five independent authorization layers — the caller's IAM policy, the bucket policy, S3 Block Public Access, Object Ownership/ACLs, the KMS key policy, and (for private access) a VPC endpoint policy. Any one of them can veto the request, and the error message rarely tells you which.

This guide gives you a deterministic order to check those layers, the exact CLI commands to inspect each one, and the fixes for the cases that cause the overwhelming majority of production 403s — especially the ones that appear after enabling encryption or turning on Block Public Access.

2. What a 403 Actually Tells You

S3 evaluates every request against the union of all applicable policies. The decision logic is: an explicit Deny anywhere always wins; otherwise you need an explicit Allow from an identity-based or resource-based policy. No matching Allow = implicit deny = 403.

3. Common Causes

4. Step-by-Step Diagnosis and Fix

Step 1: Confirm who is actually calling

# Which identity is the request using? (role vs user matters)
aws sts get-caller-identity

# Reproduce the exact failing call with debug to see the request/response
aws s3api get-object --bucket my-bucket --key path/to/key /tmp/out --debug 2>&1 | tail -40

Step 2: Simulate the policy decision (fastest root-cause tool)

# IAM Policy Simulator via CLI — tells you Allow/Deny and WHICH policy decided
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/app-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/path/to/key

Step 3: Verify the IAM policy uses the correct ARNs

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"          // OBJECT actions -> /*
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-bucket"             // LIST -> bucket ARN
    }
  ]
}

Step 4: Inspect the bucket policy for explicit Deny

aws s3api get-bucket-policy --bucket my-bucket \
  --query Policy --output text | python -m json.tool

# Watch for a common TLS-only deny that 403s any non-HTTPS request:
# "Effect": "Deny", "Condition": {"Bool": {"aws:SecureTransport": "false"}}
# and account/VPC restrictions like aws:SourceVpce or aws:PrincipalAccount.

Step 5: Check Block Public Access and Object Ownership

# Block Public Access overrides public policies/ACLs when true
aws s3api get-public-access-block --bucket my-bucket

# Object Ownership — "BucketOwnerEnforced" disables ACLs entirely
aws s3api get-bucket-ownership-controls --bucket my-bucket

# If a cross-account upload left ownership with the uploader:
aws s3api get-object-acl --bucket my-bucket --key path/to/key

Step 6: Fix KMS permissions for SSE-KMS buckets

If the bucket enforces SSE-KMS, S3 access is not enough — the caller also needs key permissions. A 403 that started right after enabling encryption is almost always this.

# Find the bucket's default KMS key
aws s3api get-bucket-encryption --bucket my-bucket

# The principal needs these on the KMS key (via key policy or IAM):
#   kms:Decrypt           (GetObject)
#   kms:GenerateDataKey   (PutObject)
{
  "Effect": "Allow",
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "arn:aws:kms:us-east-1:111122223333:key/KEY-ID"
}

5. Verification Steps

# List (needs s3:ListBucket on the bucket ARN)
aws s3 ls s3://my-bucket/path/

# Download (needs s3:GetObject on the object ARN + kms:Decrypt if SSE-KMS)
aws s3 cp s3://my-bucket/path/to/key /tmp/ok

# Upload (needs s3:PutObject + kms:GenerateDataKey if SSE-KMS)
echo test | aws s3 cp - s3://my-bucket/path/verify.txt

# Re-run the simulator and confirm it now returns "allowed"
aws iam simulate-principal-policy --policy-source-arn <role-arn> \
  --action-names s3:GetObject --resource-arns arn:aws:s3:::my-bucket/path/to/key

6. Common Mistakes

7. Prevention Tips

8. FAQ

I can download objects but "aws s3 ls" returns Access Denied. Why?

Listing uses the s3:ListBucket action, which must be granted on the bucket ARN (arn:aws:s3:::my-bucket), not the object ARN. s3:GetObject is granted on the object ARN (arn:aws:s3:::my-bucket/*). Having one without the other produces exactly this "download works, list fails" pattern.

Access started failing right after I enabled default encryption. What changed?

The bucket is now SSE-KMS, so every read needs kms:Decrypt and every write needs kms:GenerateDataKey on the KMS key — in addition to the S3 permissions. Add those actions to the caller via the key policy or its IAM policy. S3 permissions alone will keep returning 403.

My bucket policy allows public read but objects are still 403. Why?

S3 Block Public Access is almost certainly enabled and overriding the policy. Check aws s3api get-public-access-block. If IgnorePublicAcls or RestrictPublicBuckets is true, the public grant is ignored. For genuinely public content, front the bucket with CloudFront + OAC instead of disabling Block Public Access.

9. Summary

SymptomCauseFix
GetObject 403, ListBucket okMissing action or wrong ARNObject action on bucket/*
ListBucket 403, GetObject okList on object ARNs3:ListBucket on bucket ARN
403 after enabling SSE-KMSNo KMS key permissionAdd kms:Decrypt/GenerateDataKey
Public policy ignoredBlock Public Access onUse CloudFront OAC, review BPA
Cross-account 403Only one side allowsAllow on both IAM and bucket policy

Explore More in This Category

Explore more in this category: AWS & Cloud guides. Browse all DevOps Compass articles or jump to: Kubernetes, AWS, CI/CD, Containers, Monitoring, Networking.