If you’ve granted an IAM user or role what looks like proper S3 permissions, only to receive an “Access Denied” error when trying to read or write objects, you’re not alone. I’ve debugged dozens of these cases, and the issue rarely stops at the IAM policy — S3 access denial involves multiple overlapping policy layers that I’ll break down systematically. In this post, I’ll walk through exactly what causes S3 access denied errors and how to fix each one.

The Problem

You have an IAM policy that looks correct:

{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-bucket/*"
}

But when you try to read an object, you get:

Error Type Error Message
AccessDenied Access Denied

The error is frustratingly vague. Your IAM policy allows it. So why does the request fail? The answer: six different policy layers all need to align for S3 access to work.

Why Does This Happen?

  • S3 bucket policy has an explicit Deny: The bucket policy explicitly denies the action to your principal, overriding your IAM Allow
  • Block Public Access prevents the action: If the bucket has Block Public Access enabled and you’re attempting public access, it’s silently denied
  • Object is encrypted with KMS and you lack kms:Decrypt: The object uses server-side encryption with KMS, and your IAM role doesn’t have permission to decrypt the key
  • Object ACL conflicts: In buckets without ACL disabled (rare in new accounts), object ACLs can override permissions
  • VPC endpoint policy blocking access: If accessing S3 through a VPC endpoint, the endpoint policy must also allow the action
  • SCP blocking s3 actions: An AWS Service Control Policy in your Organizations denies all s3:* actions

The Fix

Step 1: Check the Bucket Policy

Get the bucket policy and look for any explicit Deny statements:

aws s3api get-bucket-policy \
  --bucket my-bucket \
  --query 'Policy' \
  --output text | jq .

If the policy doesn’t exist, get-bucket-policy returns an error (which is fine). If it does exist, look for statements like:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "*"
}

An explicit Deny anywhere in the policy blocks the action. Even if your IAM policy says Allow, a bucket Deny wins. Work with your bucket owner to modify or remove the Deny.

Step 2: Check Block Public Access

If you’re attempting public access (no AWS credentials), Block Public Access will silently deny it:

aws s3api get-public-access-block \
  --bucket my-bucket \
  --query 'PublicAccessBlockConfiguration' \
  --output text

If any of these are True, public access is blocked:

  • BlockPublicAcls
  • IgnorePublicAcls
  • BlockPublicPolicy
  • RestrictPublicBuckets

This is a feature, not a bug — it’s good security. But if you need public access, either disable Block Public Access or use CloudFront as a proxy.

Step 3: Check Object Encryption

Get metadata on the object to see if it’s encrypted:

aws s3api head-object \
  --bucket my-bucket \
  --key myfile.txt \
  --output text

Look for these fields in the output:

  • ServerSideEncryption (e.g., “aws:kms”)
  • SSEKMSKeyId (e.g., “arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012”)

If the object is encrypted with KMS, you need kms:Decrypt permission on that key. Add this to your IAM role:

{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
}

Or better, use a wildcard if you have many keys:

{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "arn:aws:kms:*:123456789012:key/*"
}

Step 4: Check VPC Endpoint Policy

If you’re accessing S3 through a VPC endpoint (not the public internet), the endpoint policy must also allow the action:

aws ec2 describe-vpc-endpoints \
  --filters Name=service-name,Values=com.amazonaws.us-east-1.s3 \
  --query 'VpcEndpoints[0].PolicyDocument' \
  --output text | jq .

If the endpoint policy has restrictive Allow statements, ensure your principal is included. A typical restrictive endpoint policy looks like:

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::123456789012:role/MyRole"
  },
  "Action": "s3:*",
  "Resource": "*"
}

Step 5: Use Access Analyzer to Debug

AWS IAM Access Analyzer can help identify public access issues:

# Get the analyzer ARN for your account
aws accessanalyzer list-analyzers \
  --output text

# Validate your bucket policy against the analyzer
aws accessanalyzer validate-policy \
  --policy-document file://bucket-policy.json \
  --policy-type IDENTITY_POLICY \
  --output text

Permission Stack

Think of S3 permissions like this:

Effective Permission = (IAM Policy) AND (Bucket Policy) AND (Object ACL) AND (KMS Key Policy) AND (VPC Endpoint Policy) AND (SCP)

All layers must allow the action. If any single layer is missing an Allow or has an explicit Deny, the request fails.

Is This Safe?

Yes. Using get-bucket-policy, head-object, and get-public-access-block are read-only operations. They don’t modify anything, so they’re completely safe for troubleshooting.

Key Takeaway

S3 access denied errors involve six overlapping policy layers: IAM policy, bucket policy, object encryption (KMS key policy), object ACL, VPC endpoint policy, and SCP. Always start by checking the bucket policy for explicit Deny statements and checking if the object uses KMS encryption. In my experience, KMS permission gaps are the second most common culprit after bucket policies — the error message doesn’t mention KMS at all, which makes it easy to miss.


Have questions or ran into a different IAM issue? Connect with me on LinkedIn or X.