DEV Community

Muskan Bandta
Muskan Bandta

Posted on

S3 Access Denied Troubleshooting: Every Cause and How to Fix It (2026)

If you are staring at An error occurred (AccessDenied) when calling the GetObject operation: Access Denied, this guide walks through every cause in the order you should check them, with the exact fix for each. I am a cloud associate and I debug this error often enough that I keep a mental checklist. Here it is, written down.

Quick answer: the 8 most common causes of S3 Access Denied

In Amazon S3, "Access Denied" means the request was authenticated but not authorized, or an explicit deny blocked it. In practice it is almost always one of these, roughly in order of frequency:

  1. The IAM identity (user or role) is missing the required s3: permission.
  2. The bucket policy does not allow the action, or explicitly denies it.
  3. S3 Block Public Access is on and you expected public/anonymous access.
  4. SSE-KMS: you have S3 permission but not kms:Decrypt on the encryption key.
  5. Missing s3:ListBucket, which turns a "key not found" into a 403.
  6. Cross-account access where only one side grants permission.
  7. An explicit Deny somewhere wins (SCP, permissions boundary, VPC endpoint policy, or bucket policy).
  8. Object ownership / ACLs after a cross-account upload.

If you only remember one thing: an explicit Deny anywhere in the chain always beats an Allow. Start by finding a deny, then work down the list.

Step 0: Confirm which identity is actually making the request

Before touching any policy, confirm who you are. Most "but I have admin" cases are the wrong principal.

aws sts get-caller-identity
Enter fullscreen mode Exit fullscreen mode

Check the Arn in the output. If it is a role you did not expect (an EC2 instance profile, a CI role, an assumed role), you have been debugging the wrong identity's permissions the whole time. This single command saves more time than any other step.

Step 1: Does the IAM identity policy allow the action?

S3 needs the specific action for the specific resource. The two resource types trip people up:

  • Bucket-level actions (s3:ListBucket, s3:GetBucketLocation) target the bucket ARN: arn:aws:s3:::my-bucket
  • Object-level actions (s3:GetObject, s3:PutObject, s3:DeleteObject) target the object ARN: arn:aws:s3:::my-bucket/*

A policy that grants s3:GetObject on arn:aws:s3:::my-bucket (no /*) will fail with Access Denied, because objects live under the /* ARN. A minimal working read policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::my-bucket"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Verify it without guessing using the IAM Policy Simulator, or from the CLI against the exact ARN you are calling.

Step 2: The ListBucket gotcha (why you get 403 instead of 404)

This is the most misleading S3 error there is. If you request an object that does not exist:

  • With s3:ListBucket, S3 returns 404 NoSuchKey.
  • Without s3:ListBucket, S3 returns 403 Access Denied, on purpose, so it does not leak whether the key exists.

So if you are sure your GetObject permission is correct but still see Access Denied, check two things: is the object key exactly right (case-sensitive, no leading slash), and do you have ListBucket? Very often the "permission" problem is actually a typo in the key, masked as a 403.

Step 3: The bucket policy

The bucket policy is a resource-based policy attached to the bucket. Two failure modes:

  • It contains an explicit "Effect": "Deny" that matches your principal or request. This wins over everything.
  • For anonymous or cross-account callers, it simply does not Allow the action.

A common accidental deny is a policy that enforces TLS or a specific VPC and denies everything else:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
Enter fullscreen mode Exit fullscreen mode

If your client is not using HTTPS, that condition flips and you are denied. Read every Deny statement's conditions carefully.

Step 4: SSE-KMS, the silent Access Denied

If the bucket uses server-side encryption with a KMS key (SSE-KMS), S3 permissions are not enough. You also need permission on the KMS key:

  • To read encrypted objects: kms:Decrypt
  • To write encrypted objects: kms:GenerateDataKey (and usually kms:Decrypt)

Symptoms: full S3 access, correct policy, and still Access Denied on GetObject or PutObject. The fix is to add the caller to the KMS key policy (or grant the KMS actions in the identity policy, if the key policy delegates to IAM). This one accounts for a huge share of "I have every S3 permission and it still fails" tickets.

Step 5: S3 Block Public Access

If you expected anonymous or public access and get Access Denied, check Block Public Access at both the account and bucket level. When it is on, it overrides any bucket policy or ACL that grants public access, by design. For genuinely public content the modern answer is usually not to make the bucket public at all, but to serve it through CloudFront with Origin Access Control. If you truly need public access, you must relax Block Public Access first, then the policy takes effect.

Step 6: Cross-account access needs both sides to agree

For same-account access, an Allow in either the identity policy or the bucket policy is enough (absent any deny). For cross-account access, you need both:

  1. The bucket's account must Allow your principal in the bucket policy (or KMS key policy for SSE-KMS).
  2. Your account's identity policy must Allow the S3 action too.

If either side is missing, you get Access Denied. Check both accounts, not just the one you are logged into.

Step 7: Object ownership after a cross-account upload

Historically, when account B uploaded an object into account A's bucket, the object stayed owned by B, and A got Access Denied reading its own bucket. If you hit this, set Bucket owner enforced (Object Ownership), which disables ACLs and makes the bucket owner own every object. This is the default for buckets created since 2023 and it removes an entire class of ACL headaches. If you are on a legacy bucket with ACLs enabled, either switch to bucket-owner-enforced or have the uploader set --acl bucket-owner-full-control.

Step 8: The org-level denies people forget

If the policies look perfect and it still fails, an explicit deny is coming from higher up:

  • Service Control Policies (SCPs) at the AWS Organizations level can deny S3 actions for the whole account, and you may not have visibility into them. Ask whoever owns the org.
  • Permissions boundaries on your IAM role cap what it can do regardless of its attached policy.
  • VPC endpoint policies: if you reach S3 through a gateway or interface endpoint, that endpoint has its own policy that can deny the bucket or action.

CloudTrail is your friend here. Look up the failed event and check the error detail, it often names the policy type that caused the deny.

How to debug it systematically (the 60-second flow)

  1. aws sts get-caller-identity to confirm the principal.
  2. Reproduce with the AWS CLI to rule out SDK/app config: aws s3api get-object --bucket my-bucket --key path/to/key out.bin
  3. Check CloudTrail for the denied event and read which policy blocked it.
  4. Run the IAM Policy Simulator for that principal, action, and exact ARN.
  5. Walk Steps 1 through 8 above in order, stopping at the first deny you find.

Frequently asked questions

Why do I get S3 Access Denied when I am an admin?
Almost always because you are not the principal you think you are (an assumed role or instance profile is making the call), or an explicit Deny from an SCP, permissions boundary, or bucket policy overrides your admin allow. Run aws sts get-caller-identity first.

What is the difference between 403 and 404 on S3?
404 NoSuchKey means the object does not exist and you have ListBucket. 403 Access Denied can mean you lack permission, or that the object is missing and you lack ListBucket so S3 hides the difference. Add s3:ListBucket to tell them apart.

Why Access Denied even though my bucket is public?
Block Public Access is almost certainly on at the account or bucket level, and it overrides public bucket policies and ACLs by design.

I have full S3 permissions but still get Access Denied on GetObject. Why?
The bucket is encrypted with SSE-KMS and you are missing kms:Decrypt on the key. S3 permission and KMS permission are separate.

How do I fix Access Denied when uploading from another account?
Grant your principal in the bucket policy of the destination account, ensure your own identity policy allows the action, and set the destination bucket to Bucket owner enforced so you own the uploaded objects.

Key takeaways

  • Access Denied = authenticated but not authorized, or an explicit deny won.
  • Confirm the calling principal before anything else.
  • An explicit Deny (bucket policy, SCP, permissions boundary, VPC endpoint) always beats an Allow.
  • Missing s3:ListBucket disguises a missing object as a 403.
  • SSE-KMS needs kms:Decrypt / kms:GenerateDataKey on top of S3 permissions.
  • Cross-account needs both the resource policy and the identity policy to allow.

Work the list top to bottom and S3 Access Denied stops being mysterious. If you have hit a cause that is not on this list, drop it in the comments and I will add it.

Top comments (2)

Collapse
 
merbayerp profile image
Mustafa ERBAY

The 403 vs 404 behavior is probably the most useful part of this checklist. I’ve seen people spend way too much time reviewing IAM policies when the actual problem was simply a wrong object key hidden behind a 403 because ListBucket wasn’t allowed.

Starting with aws sts get-caller-identity is also good advice. Checking permissions for the wrong role is an embarrassingly easy rabbit hole to fall into 😄

Nice checklist — bookmarking this one.

Collapse
 
muskan_bandta profile image
Muskan Bandta

Yeah, the 403-hiding-a-404 one still gets me. If GetObject 403s and I'm sure the policy's right, A trailing slash or wrong case fakes a permissions error way more often than a missing permission does.
get-caller-identity is my measure-twice step for that exact wrong-role reason. Thanks for the bookmark.