What the Access Denied Error Actually Means
AWS S3 troubleshooting is a maze now with all the conflicting advice flying around. As someone who has spent three years debugging AWS infrastructure, I learned everything there is to know about the aws s3 access denied error — including why it’s so maddening to pin down. Today, I will share it all with you.
But what is the Access Denied error, really? In essence, it’s AWS telling you that something in its permission evaluation chain said no. But it’s much more than that. AWS intentionally keeps the message vague for security reasons — it won’t reveal whether the block is coming from your IAM policy, the bucket policy, ACLs, or Block Public Access settings. Just “Access Denied.” Nothing else.
The actual culprit is always one of four things:
- Your identity-based IAM policy doesn’t grant the required S3 action
- The bucket’s resource-based policy explicitly denies you
- An ACL is misconfigured (less common now)
- Account-level or bucket-level Block Public Access settings are blocking the operation
Stop guessing. Start triaging systematically.
Step 1 — Check Your IAM Policy for the Right S3 Actions
Frustrated by vague error messages, I started using the AWS CLI to simulate permissions before actually running the operation — using nothing fancier than a terminal window and a copied ARN. It saves hours.
Run this command to test whether your current AWS identity can perform an action:
aws iam simulate-principal-policy
--policy-source-arn arn:aws:iam::123456789012:user/your-username
--action-names s3:GetObject
--resource-arns arn:aws:s3:::my-bucket/path/to/object.txt
The response shows either allowed or implicitDeny. Immediately tells you whether IAM is the problem. No digging required.
If it’s denied, your IAM policy needs the right S3 actions. Here’s a minimal working policy for common operations:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-bucket"
}
]
}
Notice two separate statements. One targets the bucket itself for ListBucket. The other targets objects inside it. Missing this distinction breaks permissions silently — and AWS won’t tell you why.
Identity-based policies attach directly to IAM entities: a user, a role, an assumed role. Resource-based policies live on the bucket itself and define who can reach it from outside. Both must allow the action. Either one denying it means Access Denied. That’s what makes IAM policy evaluation so unforgiving for engineers learning the system.
Step 2 — Audit the Bucket Policy for Conflicting Deny Statements
Worth saying out loud before I go further. A bucket policy can override your IAM permissions entirely — and it does so quietly.
Pull your bucket policy:
aws s3api get-bucket-policy
--bucket my-bucket
--query Policy
--output text | jq.
An explicit Deny in a bucket policy always wins. Your IAM policy says Allow, the bucket policy says Deny — you’re blocked. AWS evaluates everything and applies the most restrictive result. No exceptions.
Look for Deny statements with tricky condition keys. Here’s a real example that trips people up:
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"aws:sourceVpc": "vpc-12345678"
}
}
}
This denies access to anyone not coming from that specific VPC. If you’re outside it, Access Denied — even with perfect IAM permissions. The condition key aws:sourceVpc is doing all the blocking work without announcing itself. I’ve seen teams spend half a day on exactly this scenario.
Search your bucket policy for any Deny statements. Check the condition keys against your actual access pattern. Accessing from a Lambda function in vpc-99887766 when the policy restricts to vpc-12345678? Blocked. Completely. Don’t make my mistake and assume IAM is always the first place to look.
Step 3 — Check Block Public Access Settings at Both Levels
AWS has Block Public Access controls at two levels: the account level and the bucket level. Either one can override everything else you’ve configured.
Check bucket-level settings via CLI:
aws s3api get-public-access-block
--bucket my-bucket
If you see "BlockPublicAcls": true, public ACLs won’t work — even if your bucket policy explicitly allows public read. Account-level settings override bucket-level settings. If the account has Block Public Access fully enabled, you cannot turn it off at the bucket level alone.
Check account-level settings:
aws s3api get-account-public-access-block
In the AWS console, navigate to S3 → Block Public Access settings. You’ll see toggles for blocking public ACLs, public bucket policies, and public access generally. Each one independently restricts access based on publicness — not identity.
When does disabling these make sense? Rarely. They exist specifically to stop accidental data exposure. Disable them only for a static website CDN or a public file sharing service — and even then, use signed URLs or CloudFront to keep access controlled. Never disable Block Public Access just to make an error disappear. That’s how S3 buckets end up in breach reports.
Still Blocked — Try These Edge Case Fixes
KMS-Encrypted Objects
If your S3 objects use a customer-managed KMS key, you need two permissions: s3:GetObject and kms:Decrypt. Both. The IAM policy must include both. Here’s the KMS piece:
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
I’m apparently someone who forgot this twice — and a missing KMS permission will absolutely surface as Access Denied, not as a decryption error. S3 retrieves the object fine. Then it can’t decrypt it. Then it blames access. Maddening.
Cross-Account Access
Accessing an S3 bucket from a different AWS account requires the bucket policy to explicitly list your account’s ARN as a principal. Your IAM policy alone isn’t enough — the bucket policy must actively trust you.
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/my-cross-account-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::their-bucket/*"
}
Accessing from account 111111111111 into a bucket owned by account 222222222222? That statement must exist in the bucket policy. Without it, you’re blocked at the bucket level regardless of your IAM setup. That’s what makes cross-account S3 access endearing to us infrastructure engineers — two separate trust relationships, both required, neither optional.
S3 Object Ownership Override
S3 Object Ownership changed how bucket owner-enforced access works. BucketOwnerEnforced mode means ACLs are completely ignored. Full stop. Your IAM policy and bucket policy are what matter — nothing else.
Check this setting:
aws s3api get-bucket-ownership-controls
--bucket my-bucket
If you’re relying on ACLs to grant access, you’re wasting time. Switch to IAM policies and bucket policies. That new approach took off several years ago and eventually evolved into the standard that AWS engineers know and recommend today.
So, here’s the order: start with the IAM simulation. Then check the bucket policy Deny statements. Then check Block Public Access. Work through those three layers in sequence and you’ll find the block point in minutes instead of hours. The error message won’t help you — but the AWS CLI will.
Leave a Reply