There are few experiences in cloud development more frustrating than the generic Amazon S3 "403 Forbidden" or "Access Denied" error. It is the silent killer of productivity.
The error message rarely tells you why access was blocked. It simply states that the door is locked, leaving you to guess which of the half-dozen keys you are holding is the wrong one.
You might have full Administrator access, yet S3 still denies your request. This guide moves beyond basic troubleshooting. We will dissect the AWS authorization logic and provide a rigorous, 5-step checklist to resolve S3 403 errors permanently.
The Root Cause: How AWS Evaluates S3 Requests
To fix the problem, you must understand the "Decision Logic" AWS uses. S3 permissions are not binary. When you make a request (e.g., GetObject), AWS evaluates permissions from multiple distinct layers simultaneously.
If any of the following layers result in an explicit Deny, the request fails. If none result in an explicit Allow, the request fails (Implicit Deny).
- IAM User/Role Policies: Does the entity making the call have permission?
- S3 Bucket Policies: Does the target bucket explicitly allow or deny this entity?
- S3 Block Public Access settings: Are there account-level or bucket-level blocks overrides?
- S3 Access Control Lists (ACLs): Does the bucket owner actually own the object?
- KMS Key Policies: If encrypted, does the user have permission to use the decryption key?
Here is the checklist to isolate the failure.
Step 1: Verify IAM Identity Permissions
The most common cause is a malformed Identity-based Policy attached to your User or Role. A frequent mistake is confusing Bucket-level permissions with Object-level permissions.
The Nuance
s3:ListBucket applies to the bucket resource. s3:GetObject applies to the objects inside the bucket.
If your policy resource is arn:aws:s3:::my-bucket, you cannot read files. You must include the wildcard /*.
The Fix
Audit the IAM policy attached to your user or role. Ensure you handle both resource types.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListingOfBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::production-app-assets"
},
{
"Sid": "AllowReadingObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::production-app-assets/*"
}
]
}
Validation
Use the AWS CLI to simulate the policy. This confirms if IAM is the blocker without guessing.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/MyAppRole \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::production-app-assets/config.json
Step 2: Analyze the S3 Bucket Policy
Even if your IAM user has AdministratorAccess, an S3 Bucket Policy can explicitly deny you. Bucket policies override IAM permissions when an Explicit Deny is present.
The Common Pitfall
A Bucket Policy intended to restrict access to a specific VPC Endpoint or IP address often accidentally locks out IAM users who are outside that network context (like your local machine).
The Fix
Check for Effect: Deny statements. If a statement denies access based on aws:SourceIp or aws:SourceVpce, ensure your current connection meets those criteria or add a condition to exempt your specific IAM User/Role.
Here is a safe policy pattern that enforces SSL but won't lock you out:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureConnections",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::production-app-assets",
"arn:aws:s3:::production-app-assets/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
If you see a NotPrincipal block, be extremely careful. NotPrincipal with Deny is dangerous and often results in locking out the root account.
Step 3: Check S3 Block Public Access
"Block Public Access" (BPA) is a security layer that sits above standard policies. It exists at two levels: the Bucket level and the Account level.
The Conflict
If your Bucket Policy tries to grant public read access (e.g., for a static website), but BPA is enabled, S3 will return a 403 Forbidden immediately. It ignores the Allow statement in your policy.
The Fix
If your bucket is private, ensure all four BPA settings are On (True). If your bucket is public, specific settings must be Off (False).
You can verify this quickly via the CLI:
# Check Bucket Level
aws s3api get-public-access-block --bucket production-app-assets
# Check Account Level (often overlooked)
aws s3control get-public-access-block --account-id 123456789012
To resolve a conflict where you need public access, you must delete the block configuration:
aws s3api delete-public-access-block --bucket production-app-assets
Note: Always prefer using CloudFront with Origin Access Control (OAC) rather than making S3 buckets directly public.
Step 4: Verify Object Ownership (ACLs)
This is the classic "Cross-Account Upload" issue.
If Account A owns the bucket, but Account B uploads a file, Account B owns the object. By default, Account A (the bucket owner) cannot read that object and will receive a 403 error, despite owning the bucket.
The Modern Solution
AWS now recommends disabling ACLs entirely using the S3 Object Ownership setting. This forces the bucket owner to own every object uploaded to it, regardless of who uploaded it.
The Fix
Change the Object Ownership setting to BucketOwnerEnforced. This disables ACLs and relies solely on Policies.
aws s3api put-bucket-ownership-controls \
--bucket production-app-assets \
--ownership-controls="Rules=[{ObjectOwnership=BucketOwnerEnforced}]"
Once applied, ACLs are ignored, and the 403 error caused by mixed ownership disappears.
Step 5: The Silent Killer – AWS KMS
If your bucket uses Server-Side Encryption with a Customer Managed Key (SSE-KMS), having S3 permissions is not enough. You also need cryptographic permission to use the key.
If you request an object and lack kms:Decrypt permission, S3 returns a generic 403 Forbidden. It does not tell you it was a KMS failure for security reasons.
The Fix
- Identify which KMS Key ID is encrypting the bucket/object.
- Ensure your IAM Role has the following permissions in its Identity Policy AND the KMS Key Policy allows the role.
Required IAM/Key Permissions:
{
"Sid": "AllowKMSEncryptDecrypt",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}
If you are using a custom KMS Key Policy, ensure the Principal (your role) is explicitly listed in the Key Policy's "Key Users" statement.
Summary and Conclusion
S3 403 errors are rarely a bug in S3; they are a feature of AWS's "Defense in Depth" architecture. To debug effectively, stop guessing and start auditing layers.
- IAM: Check
/*vs bucket ARN. - Bucket Policy: Look for Explicit Denys.
- Public Access: Ensure BPA settings don't override your intentions.
- Ownership: Switch to
BucketOwnerEnforcedto kill ACL issues. - KMS: Verify
kms:Decryptpermissions.
By methodically validating these five layers, you can turn ambiguous Access Denied errors into solvable configuration updates.