Error Medic

AWS S3 Access Denied (403 Forbidden): Complete Troubleshooting Guide

Fix AWS S3 Access Denied and 403 Forbidden errors fast. Diagnose IAM policies, bucket policies, KMS keys, Block Public Access, throttling, and timeouts step by

Last updated:
Last verified:
2,042 words
Key Takeaways
  • AWS S3 'Access Denied' (HTTP 403) is caused by a conflict in one of five policy layers — IAM identity policies, S3 bucket policies, S3 ACLs, Block Public Access settings, or KMS key policies. A single explicit Deny in any layer overrides all Allow statements everywhere else.
  • KMS encryption mismatches are the most frequently overlooked root cause: even perfect S3 IAM permissions will produce Access Denied if the calling role lacks kms:Decrypt or kms:GenerateDataKey on the bucket's CMK.
  • For S3 throttling (HTTP 503 SlowDown) and timeouts, use AWS SDK adaptive retry mode, randomize object key prefixes to spread load across partitions, and create an S3 VPC Gateway Endpoint to eliminate NAT bottlenecks.
Fix Approaches Compared
MethodWhen to UseTimeRisk
Add IAM policy statementCaller's IAM role lacks s3:GetObject, s3:PutObject, or s3:ListBucket2–5 minLow — additive change only
Edit S3 bucket policyBucket policy has an explicit Deny or missing Allow for the principal5–10 minMedium — syntax errors lock out all principals
Disable Block Public AccessPublic bucket or presigned URL returns 403 due to BPA override1–2 minHigh — exposes bucket publicly if misconfigured
Grant KMS key accessBucket uses SSE-KMS and caller lacks kms:Decrypt or kms:GenerateDataKey5 minLow — additive key policy or IAM change
Cross-account trust + resource policyDifferent AWS account needs access; both sides must grant permission10–15 minMedium — requires coordinated changes in two accounts
Exponential backoff + prefix sharding503 SlowDown errors on high-TPS workloads concentrated on one prefix30–60 min (code change)Low — improves reliability with no data risk

Understanding AWS S3 Access Denied Errors

When Amazon S3 returns an Access Denied error (HTTP 403 Forbidden) or a Permission Denied message, the request was authenticated but not authorized. S3 evaluates authorization across five independent policy layers: IAM identity policies, S3 bucket policies, S3 Access Control Lists (ACLs), S3 Block Public Access (BPA) settings, and AWS KMS key policies. A single explicit Deny in any layer vetoes all Allow statements across every other layer.

The exact error strings you will encounter:

  • An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
  • 403 Forbidden
  • Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied)
  • User: arn:aws:iam::123456789012:user/alice is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::my-bucket/file.txt
  • 503 SlowDown: Please reduce your request rate.
  • RequestTimeout / ReadTimeoutError / ConnectTimeoutError

Identifying which layer rejected the request is the most important first step.


Step 1: Diagnose the Root Cause

1a. Capture the Full Error

The AWS CLI --debug flag prints the full HTTP response including headers and error body:

aws s3api get-object --bucket my-bucket --key file.txt /dev/null --debug 2>&1 \
  | grep -E '(AccessDenied|403|Error|errorCode)'
1b. Use the IAM Policy Simulator

The IAM Policy Simulator evaluates all policies in effect for a principal and reports which policy is responsible for a denial:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/my-app-role \
  --action-names s3:GetObject s3:ListBucket \
  --resource-arns \
    arn:aws:s3:::my-bucket \
    "arn:aws:s3:::my-bucket/*"

Look for "EvalDecision": "explicitDeny" — this confirms a hard deny is in play and shows the MatchedStatements field that identifies the exact policy document.

1c. Inspect CloudTrail Logs

CloudTrail records every S3 API call including the authorization decision, the IAM principal, and the source IP:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=my-bucket \
  --start-time 2024-01-15T00:00:00Z \
  --query 'Events[?contains(CloudTrailEvent,`AccessDenied`)].CloudTrailEvent' \
  --output text | python3 -m json.tool

The errorCode and errorMessage fields in each CloudTrail entry identify the denying policy type.

1d. Check Block Public Access

For objects accessed via presigned URLs or public bucket policies, Block Public Access settings may silently override all other permissions:

aws s3api get-public-access-block --bucket my-bucket

All four BPA settings default to true for buckets created after April 2023.


Step 2: Fix IAM Identity Permissions

The most frequent cause of S3 Access Denied is a missing IAM permission on the calling role or user. Attach this inline or managed policy to the principal:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3BucketList",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-bucket"
    },
    {
      "Sid": "AllowS3ObjectAccess",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Critical distinction: s3:ListBucket must target the bucket ARN (arn:aws:s3:::my-bucket), while object-level actions must target the object pattern (arn:aws:s3:::my-bucket/*). Confusing these is the most common IAM misconfiguration.


Step 3: Fix the S3 Bucket Policy

A bucket policy explicit Deny overrides any IAM Allow. Inspect the existing policy:

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

Search for Deny statements that may be unintentionally broad. A safe cross-account access grant looks like:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CrossAccountRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::987654321098:role/external-role"
      },
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Always test bucket policy changes with the IAM Policy Simulator before saving, since a malformed policy can lock out all principals including the bucket owner.


Step 4: Resolve KMS Encryption Issues

If the bucket uses SSE-KMS, the calling principal needs both S3 permissions and KMS permissions. Missing KMS access surfaces identically to a missing S3 permission — the error message says Access Denied on GetObject or PutObject.

Required KMS permissions:

  • kms:Decrypt — for reading encrypted objects
  • kms:GenerateDataKey — for writing new objects
  • kms:DescribeKey — for key metadata operations
# Identify the KMS key used by the bucket
aws s3api get-bucket-encryption --bucket my-bucket

# Simulate KMS access for the calling role
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/my-app-role \
  --action-names kms:Decrypt kms:GenerateDataKey \
  --resource-arns arn:aws:kms:us-east-1:123456789012:key/mrk-abc123

If the KMS key is in a different account, you must also update the KMS key policy to allow the external principal — IAM policies alone are insufficient for cross-account KMS usage.


Step 5: Fix Cross-Account Access

For cross-account S3 access, both sides must independently grant permission. Neither side alone is sufficient:

  1. Bucket account: Add the external role as a principal in the S3 bucket policy (see Step 3).
  2. External account: Attach an IAM policy to the calling role explicitly allowing S3 actions on the bucket ARN.

Without both sides configured, S3 returns Access Denied regardless of how permissive one side is.


Step 6: Handle S3 Rate Limiting (503 SlowDown)

AWS S3 supports up to 5,500 GET/HEAD requests/sec and 3,500 PUT/COPY/POST/DELETE requests/sec per key prefix partition. When requests concentrate on one prefix, S3 returns:

503 SlowDown: Please reduce your request rate.

S3 automatically re-partitions after sustained load (~30 minutes), but you should take proactive steps:

1. Enable adaptive retry in the AWS SDK:

import boto3
from botocore.config import Config

config = Config(retries={"max_attempts": 10, "mode": "adaptive"})
s3 = boto3.client("s3", config=config)

2. Randomize key prefixes to spread load across partitions:

  • Before: logs/2024-01-15/app-001.log
  • After: a3f7/logs/2024-01-15/app-001.log (prepend hash of key)

This simple change can increase effective throughput by orders of magnitude for high-TPS workloads.


Step 7: Resolve S3 Timeouts

S3 timeouts are typically caused by large objects on slow connections, VPC NAT gateway saturation, or missing S3 VPC endpoints. Traffic routing from EC2 through a NAT gateway to S3 is both slow and costly.

Create a free S3 VPC Gateway Endpoint to route traffic directly:

aws ec2 create-vpc-endpoint \
  --vpc-id vpc-12345678 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-12345678

For SDK timeout tuning with boto3:

config = Config(
    connect_timeout=10,
    read_timeout=60,
    retries={"max_attempts": 5, "mode": "adaptive"}
)

For large file transfers (>100 MB), use multipart upload or aws s3 cp which handles multipart automatically and supports resumable transfers.

Frequently Asked Questions

bash
#!/usr/bin/env bash
# S3 Access Denied Diagnostic Script
# Usage: ./s3-diagnose.sh <bucket-name> <object-key> <iam-role-arn>

BUCKET="${1:-my-bucket}"
KEY="${2:-test/file.txt}"
ROLE_ARN="${3:-arn:aws:iam::123456789012:role/my-app-role}"

echo "=== 1. Caller Identity ==="
aws sts get-caller-identity

echo ""
echo "=== 2. Direct S3 Access Test ==="
aws s3api get-object \
  --bucket "$BUCKET" \
  --key "$KEY" \
  /dev/null 2>&1 || true

echo ""
echo "=== 3. Block Public Access Settings ==="
aws s3api get-public-access-block --bucket "$BUCKET" 2>&1

echo ""
echo "=== 4. Bucket Policy ==="
aws s3api get-bucket-policy \
  --bucket "$BUCKET" \
  --query Policy \
  --output text 2>&1 \
  | python3 -m json.tool 2>/dev/null \
  || echo "[No bucket policy or access denied to read policy]"

echo ""
echo "=== 5. Bucket Encryption (SSE-KMS?) ==="
aws s3api get-bucket-encryption --bucket "$BUCKET" 2>&1

echo ""
echo "=== 6. IAM Policy Simulator ==="
aws iam simulate-principal-policy \
  --policy-source-arn "$ROLE_ARN" \
  --action-names s3:GetObject s3:ListBucket s3:PutObject s3:DeleteObject \
  --resource-arns \
    "arn:aws:s3:::${BUCKET}" \
    "arn:aws:s3:::${BUCKET}/${KEY}" \
  --query 'EvaluationResults[*].{Action:EvalActionName,Decision:EvalDecision}' \
  --output table 2>&1

echo ""
echo "=== 7. Check for KMS Key (if SSE-KMS) ==="
KMS_KEY=$(aws s3api get-bucket-encryption --bucket "$BUCKET" \
  --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.KMSMasterKeyID' \
  --output text 2>/dev/null)
if [ -n "$KMS_KEY" ] && [ "$KMS_KEY" != "None" ]; then
  echo "Bucket KMS key: $KMS_KEY"
  aws iam simulate-principal-policy \
    --policy-source-arn "$ROLE_ARN" \
    --action-names kms:Decrypt kms:GenerateDataKey kms:DescribeKey \
    --resource-arns "$KMS_KEY" \
    --query 'EvaluationResults[*].{Action:EvalActionName,Decision:EvalDecision}' \
    --output table 2>&1
else
  echo "Bucket does not use SSE-KMS or encryption info unavailable."
fi

echo ""
echo "=== 8. Recent Access Denied Events (CloudTrail, last 2h) ==="
START=$(date -u -d '2 hours ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
  || date -u -v-2H '+%Y-%m-%dT%H:%M:%SZ')
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue="$BUCKET" \
  --start-time "$START" \
  --query 'Events[?contains(CloudTrailEvent,`AccessDenied`)].{Time:EventTime,User:Username,Event:EventName}' \
  --output table 2>&1 | head -50

echo ""
echo "=== Diagnosis complete. Look for explicitDeny in simulator and Deny statements in bucket policy. ==="
E

Error Medic Editorial

The Error Medic Editorial team comprises senior DevOps and SRE engineers with collective experience managing AWS infrastructure at scale across financial services, SaaS, and media industries. Our troubleshooting guides are built from real incident postmortems and validated against current AWS documentation and service limits.

Sources

Related Articles in AWS S3

Explore More Cloud Infrastructure Guides