Start Here: 30-Second Fix
I know this error is infuriating. You've attached the right IAM policy, you can see the bucket in the console, but any request—CLI, SDK, or even the browser—throws AccessDenied (403 Forbidden). Before you pull your hair out, check the basics that trip up even seasoned engineers.
Check Your AWS Region
Yes, it sounds dumb, but I've seen this happen twice this month alone. If your CLI or SDK is pointing to the wrong region, S3 will happily return AccessDenied instead of a clearer error. Run aws s3 ls and confirm the region in your config. A quick aws configure will let you fix it.
Check the Bucket Name Spelling
One off-by-one character in the bucket name—like my-bucket vs my-buckett—gives you a 403, not a 404. Triple-check the exact bucket ARN in your IAM policy: arn:aws:s3:::my-bucket/*. If you used a wildcard, make sure it's placed correctly—arn:aws:s3:::my-bucket/* covers objects, but arn:aws:s3:::my-bucket covers the bucket itself. You often need both.
If you're using the AWS console, click the bucket name from the list instead of typing it. That saves you from typos.
If that didn't fix it, move to the next level.
Moderate Fix: 5 Minutes
Still locked out? The problem is almost certainly outside your IAM policy. Let's look at the other three suspects: bucket policy, ACLs, and S3 Public Access Block.
Check the Bucket Policy
A bucket policy can explicitly deny access even if your IAM allows it. In the S3 console, go to your bucket → Permissions → Bucket Policy. Look for a Deny statement that might be catching your user or role. Common culprits are conditions like aws:SourceIp, aws:SecureTransport, or a principal that's too narrow.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}That's a common one—it blocks HTTP requests. If you're testing with plain HTTP, you'll get denied. But if you're using HTTPS, that's not the issue.
Inspect Object-Level ACLs
If your bucket is set to Object Ownership: Bucket owner preferred (or if ACLs are enabled), an object uploaded by another account might not be accessible to you, even with full IAM permissions. This happened to me with a shared bucket—the object owner was a different account, and my IAM allowed everything, but I still got 403.
Check the object's Permissions tab. If the ACL looks off, you can try aws s3api put-object-acl --bucket my-bucket --key my-object --acl bucket-owner-full-control from the uploader's account, or simply re-upload with that ACL.
Review S3 Public Access Block
If you're trying to make objects public and they're not, the Block Public Access settings might be silently overriding your bucket policy. Go to your bucket's Permissions tab, then Block Public Access. If any of the four blocks are enabled, they'll deny public reads even if your bucket policy allows s3:GetObject for Principal: "*".
You can unblock if it's your own bucket and you truly want it public, but think hard before doing that.
Still no luck? Time to dig into the advanced stuff.
Advanced Fix: 15+ Minutes
If you've verified IAM, bucket policy, ACLs, and public access blocks, the culprit is often something you can't see in the console directly.
Check S3 Bucket Ownership and ACL Disablement
As of April 2023, AWS defaults to disabling ACLs for new buckets. If your bucket was created earlier, ACLs may still be active. But there's a quirk: if the bucket owner doesn't own the object, even the bucket owner can't access it without an explicit grant. The fix is to update the bucket to Object Ownership: Bucket owner enforced. This disables ACLs and gives the bucket owner full control over all objects.
aws s3api put-bucket-ownership-controls --bucket my-bucket --ownership-controls '{"Rules": [{"ObjectOwnership": "BucketOwnerEnforced"}]}'This is a one-way door—you can enable it, but you can't revert to BucketOwnerPreferred afterward (you can, but it's messy). Only do this if you're confident you don't need ACLs.
Inspect AWS KMS Encryption Keys
If your bucket uses SSE-KMS, the request must have access to the KMS key. IAM permissions alone won't help—you also need kms:Decrypt (for GETs) and kms:GenerateDataKey (for PUTs) on the key. Attach those to your role or user. Check if the key policy allows your account or role. For cross-account scenarios, both the key policy and IAM policy must grant access.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}
]
}You can test with aws s3 cp s3://my-bucket/my-file - --sse aws:kms --sse-kms-key-id your-key-id to see if the error message changes.
Look for Service Control Policies (SCPs)
If you're in an AWS Organization, an SCP at the root or OU level can deny S3 actions without you knowing. Go to AWS Organizations → Policies → SCPs and check if any policy denies s3:* or s3:GetObject for your account. This often affects new members of a governed organization.
I once spent an hour debugging a 403 only to find an SCP that required MFA for all S3 actions. Always check SCPs early if you're under an org.
Still Locked Out? Use These Debugging Commands
When you're stuck, make the error tell you more. Use the AWS CLI with --debug to see the exact request and response:
aws s3 ls s3://my-bucket --debugLook for the <Error><Code>AccessDenied</Code> block. Sometimes the <Message> will hint at the cause, like “Access Denied because of bucket policy.”
You can also test with --no-sign-request to see if the bucket is publicly accessible, but that only works if the bucket allows it.
And if you've really exhausted everything, open a support case with AWS. They can see the eventual consistency and policy evaluation that you can't. But you've got a solid shot at fixing it yourself with these steps.