🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

AWS S3 403 Forbidden Error: Fix in 5 Minutes

Getting 403 Forbidden from S3 even though you're sure the bucket policy is right? Here is exactly how to diagnose IAM policy, bucket policy, ACL, block-public-access, and KMS key permission causes.

Shubham4 min read
Share:Tweet

S3 403 errors are frustrating because there are five different places permission can be denied, and AWS gives you the same generic AccessDenied message regardless of which one it is. Here is how to find the real cause fast.

Step 1: Get the Real Reason From CloudTrail

bash
# The console/CLI error is useless on its own — CloudTrail has the actual denial reason
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
  --max-results 5 \
  --query 'Events[*].CloudTrailEvent' --output text | jq .

Look for "errorCode" and "errorMessage" in the event — this tells you whether it was IAM, bucket policy, or something else denying the request.

bash
# Faster path if you have S3 server access logging or CloudTrail data events enabled:
aws s3api get-bucket-logging --bucket my-bucket

Cause 1: IAM Policy Doesn't Grant the Action

bash
# Check what the calling identity can actually do
aws sts get-caller-identity
aws iam get-user-policy --user-name my-user --policy-name my-policy
# or for roles:
aws iam get-role-policy --role-name my-role --policy-name my-policy
json
// Common mistake — policy grants s3:GetObject but not s3:ListBucket,
// so listing works in console but GetObject fails, or vice versa
{
  "Effect": "Allow",
  "Action": ["s3:GetObject"],
  "Resource": "arn:aws:s3:::my-bucket/*"
}

Fix — you usually need both the bucket-level and object-level permissions:

json
{
  "Effect": "Allow",
  "Action": ["s3:ListBucket"],
  "Resource": "arn:aws:s3:::my-bucket"
},
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-bucket/*"
}

Note the ARN difference — arn:aws:s3:::my-bucket (no /*) for bucket-level actions, arn:aws:s3:::my-bucket/* for object-level actions. Mixing these up is the single most common cause of this error.

Cause 2: Bucket Policy Explicitly Denies

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

An explicit Deny in the bucket policy always wins over an Allow in an IAM policy — this is the rule people forget.

json
{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": {
    "StringNotEquals": {
      "aws:SourceVpce": "vpce-0abc123"
    }
  }
}

If your bucket has a VPC-endpoint restriction like this and you're testing from your laptop (not through the VPC endpoint), you will always get 403 regardless of your IAM permissions. Check every Condition block carefully.

Cause 3: Block Public Access Settings

bash
aws s3api get-public-access-block --bucket my-bucket
json
{
    "PublicAccessBlockConfiguration": {
        "BlockPublicAcls": true,
        "IgnorePublicAcls": true,
        "BlockPublicPolicy": true,
        "RestrictPublicBuckets": true
    }
}

If BlockPublicPolicy is true, S3 will reject bucket policies that grant public access even if you write one — and RestrictPublicBuckets blocks public access even through an otherwise-valid policy. This is usually correct for security but breaks intentionally-public buckets (like static website hosting) if left on by default.

Fix — only if the bucket is meant to be public:

bash
aws s3api put-public-access-block --bucket my-bucket \
  --public-access-block-configuration \
  BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false

Cause 4: Bucket Owner vs Object Owner Mismatch (Cross-Account)

bash
# When another account uploaded objects to your bucket without
# specifying bucket-owner-full-control, you don't own the objects
aws s3api get-object-acl --bucket my-bucket --key uploaded-file.txt

Fix — require the uploading account to set the ACL, or enable Bucket Owner Enforced (recommended for 2026):

bash
aws s3api put-bucket-ownership-controls --bucket my-bucket \
  --ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]

BucketOwnerEnforced disables ACLs entirely and makes the bucket owner the object owner for everything uploaded — this eliminates the whole class of cross-account ownership 403s.

Cause 5: SSE-KMS Key Permissions

bash
# If the bucket uses a customer-managed KMS key for encryption,
# s3:GetObject succeeding requires kms:Decrypt on that key too
aws kms get-key-policy --key-id alias/my-bucket-key --policy-name default
json
{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::123456789012:role/my-app-role"},
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "*"
}

Missing kms:Decrypt on the key policy is a frequent cause when everything on the S3 side looks correct — the IAM role has s3:GetObject, the bucket policy allows it, but the KMS key policy never granted decrypt access to that role.

Diagnostic Checklist

bash
aws sts get-caller-identity                                    # who are you actually?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/my-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/my-key    # simulates the exact call
aws s3api get-bucket-policy --bucket my-bucket
aws s3api get-public-access-block --bucket my-bucket
aws kms get-key-policy --key-id alias/my-key --policy-name default

iam simulate-principal-policy is the fastest way to cut through all five causes at once — it tells you exactly which policy is denying the request.


More AWS troubleshooting? Read our AWS RDS connection timeout fix and AWS ECR push denied authentication fix.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments