How to fix “Access Denied” (403) errors in S3

TL;DR

A 403 AccessDenied from Amazon S3 means the request was signed with real credentials but no policy allowed it, or something explicitly denied it. Work through the causes in this order: the wrong identity or profile; an IAM policy that puts s3:ListBucket on the object ARN instead of the bucket ARN, or s3:GetObject without the /* ARN; an explicit Deny in the bucket policy; Block Public Access; an SSE-KMS key you cannot decrypt; an object owned by another account; and an SCP, permission boundary or VPC endpoint policy. Two traps: S3 answers 403 instead of 404 for a missing object when you lack s3:ListBucket, and a wrong region or endpoint can surface as 403. S3 Viewer separates the two big cases for you: “The server rejected these credentials” means the key or secret is wrong, while “The server credentials don't allow this” means the key is valid and a policy said no.

Steps

Step-by-step.

  1. 01

    Confirm which identity is making the call

    A surprising share of 403s are the wrong profile, an expired SSO session, or an instance or container role you did not expect. Ask STS who you are before touching any policy. In S3 Viewer the identity is the access key saved on that server connection, so check which key you pasted.
    aws sts get-caller-identity
    # {
    #   "UserId": "AIDA...",
    #   "Account": "123456789012",
    #   "Arn": "arn:aws:iam::123456789012:user/s3-viewer-readonly"
    # }
  2. 02

    Reproduce with the smallest possible call

    Find out which action is denied. Bucket-level and object-level permissions are separate, and aws s3 ls with no bucket needs a third one, s3:ListAllMyBuckets, which is often the whole problem.
    aws s3api head-bucket --bucket my-bucket
    aws s3api list-objects-v2 --bucket my-bucket --prefix exports/ --max-keys 1
    aws s3api get-object --bucket my-bucket --key exports/q3.pdf /tmp/q3.pdf
  3. 03

    Check the IAM policy's actions and ARNs

    The most common cause. s3:ListBucket applies to the bucket ARN (arn:aws:s3:::my-bucket), while s3:GetObject, s3:PutObject and s3:DeleteObject apply to the object ARN (arn:aws:s3:::my-bucket/*). Put an action on the wrong ARN and it silently grants nothing. Versioned buckets also need s3:GetObjectVersion to read old versions.
    {
      "Version": "2012-10-17",
      "Statement": [
        { "Effect": "Allow",
          "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
          "Resource": "arn:aws:s3:::my-bucket" },
        { "Effect": "Allow",
          "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
          "Resource": "arn:aws:s3:::my-bucket/*" }
      ]
    }
  4. 04

    Look for an explicit Deny in the bucket policy

    An explicit Deny anywhere beats every Allow. The usual suspects are conditions on aws:SourceIp or aws:SourceVpce that block your laptop or a server outside the VPC, aws:SecureTransport denies hitting plain-HTTP clients, and aws:PrincipalOrgID blocking a principal from another organization. For cross-account access, the caller's IAM policy and the bucket policy must both allow the action.
    aws s3api get-bucket-policy --bucket my-bucket --query Policy --output text | jq .
  5. 05

    If the object is encrypted with SSE-KMS, check the key

    Listing works but every download is 403? That is almost always KMS. Reading an object encrypted with a customer managed key needs kms:Decrypt on that key (uploads also need kms:GenerateDataKey), and the key policy itself must allow your principal. Cross-account KMS needs both sides, like a bucket policy.
    aws s3api head-object --bucket my-bucket --key exports/q3.pdf
    # "ServerSideEncryption": "aws:kms",
    # "SSEKMSKeyId": "arn:aws:kms:us-east-1:123456789012:key/..."
  6. 06

    Check who owns the object and whether public access is blocked

    Objects uploaded by another account under the legacy ACL model can be unreadable even by the bucket owner. Fix it once by setting Object Ownership to Bucket owner enforced, which disables ACLs and makes the bucket owner own everything. Separately, Block Public Access (on by default since 2023) turns public-read policies and ACLs into 403s for anonymous requests; that is the feature working as intended.
  7. 07

    Check the guardrails outside S3

    Service control policies, permission boundaries, session policies on an assumed role, and VPC endpoint policies can all deny a call that every S3-level policy allows. Requester Pays buckets return 403 unless you send --request-payer requester. Since 2021 the AccessDenied message usually names the policy type that denied you (“with an explicit deny in a service control policy”, “because no identity-based policy allows the s3:GetObject action”), so read the full message. CloudTrail records the same detail under errorMessage.
  8. 08

    Rule out a 403 that is really a 404, or a wrong region

    S3 deliberately answers GetObject on a key that does not exist with 403 unless the caller has s3:ListBucket, so that unauthorized callers cannot probe for key names. If listing is not something you are meant to have, check the key spelling before the policies. A bucket in another region usually fails with 301 or 400, but some clients surface it as 403, so confirm the endpoint matches the bucket's location.
    aws s3api get-bucket-location --bucket my-bucket
    # { "LocationConstraint": "eu-west-1" }   # null means us-east-1
  9. 09

    In S3 Viewer, read which of the two messages you got

    “The server rejected these credentials” maps to InvalidAccessKeyId or SignatureDoesNotMatch: the key or secret is wrong, rotated, deleted, or pasted with a stray space. Fix the keys, not the policy. “The server credentials don't allow this” is a genuine AccessDenied: the key is valid and a policy said no, so work through steps 3 to 8. When connecting, if the key cannot list buckets S3 Viewer asks for bucket names and checks each with HeadBucket; a bucket that fails must be granted or removed from the list, and “Bucket name was not found” is a 404, so check the spelling and endpoint.
Under the hood

What's actually happening.

Every S3 request is authorized by evaluating all applicable policies together: the caller's identity-based policies, the bucket policy, object and bucket ACLs where they are still enabled, any session policy, plus the organization's service control policies, permission boundaries and the VPC endpoint policy if the call came through one. The rule is simple: an explicit Deny anywhere wins; otherwise at least one Allow must match the action and the resource ARN; and for a caller in another account, both that caller's policies and the bucket policy must allow it.


Resources are where most people slip. Bucket operations (ListBucket, GetBucketLocation) match arn:aws:s3:::my-bucket; object operations match arn:aws:s3:::my-bucket/*; and ListAllMyBuckets only matches *. Encryption adds a second authorization: reading an SSE-KMS object needs a KMS decrypt that succeeds under the key policy, and a KMS failure is reported by S3 as a plain 403.


S3 Viewer classifies the error codes it gets back from the bucket. Credential failures (InvalidAccessKeyId, SignatureDoesNotMatch, ExpiredToken) become “The server rejected these credentials”; AccessDenied becomes “The server credentials don't allow this”; a 404 on a bucket becomes “Bucket not found”. When a key cannot call ListBuckets, connecting falls back to HeadBucket on each bucket you name, so prefix- or bucket-scoped credentials work without any wildcard permissions.

FAQ

Common questions.

What does “Access Denied” mean in Amazon S3?

The request was authenticated (S3 recognised the access key and the signature was valid) but not authorized: no identity policy, bucket policy or ACL allowed the action, or a policy explicitly denied it. It is HTTP 403 with the error code AccessDenied. Bad credentials produce different codes, InvalidAccessKeyId or SignatureDoesNotMatch.

Why do I get Access Denied when my IAM policy grants full S3 access?

Because an Allow is not the last word. An explicit Deny in a bucket policy, service control policy, permission boundary, session policy or VPC endpoint policy overrides it; an SSE-KMS key you cannot decrypt fails as 403; and cross-account access needs the bucket policy to allow you too. Also confirm with aws sts get-caller-identity that the policy is attached to the identity actually making the call.

Why does S3 return 403 instead of 404 for a file that doesn't exist?

By design. If the caller lacks s3:ListBucket on the bucket, S3 returns 403 for a missing key rather than 404, so that an unauthorized caller cannot discover which keys exist. Grant s3:ListBucket on the bucket ARN and the same request returns 404 NoSuchKey.

Why can I list the bucket but not download objects?

Three usual causes: the policy grants s3:GetObject on the bucket ARN instead of arn:aws:s3:::my-bucket/*; the objects are encrypted with an SSE-KMS key you lack kms:Decrypt on; or the objects were uploaded by another account under the legacy ACL model and you are not their owner. Switch Object Ownership to Bucket owner enforced to fix the third.

Why do I get Access Denied on aws s3 ls?

aws s3 ls with no bucket calls ListBuckets, which needs s3:ListAllMyBuckets on Resource "*". That is a different permission from s3:ListBucket on one bucket. A least-privilege user usually does not have it, so run aws s3 ls s3://my-bucket/ instead. S3 Viewer handles this case by asking you to name the buckets the key can reach.

Why does my public S3 bucket still return Access Denied?

Most often Block Public Access is on, which is the default for new buckets and overrides any public bucket policy or ACL. Otherwise the bucket policy grants s3:GetObject on the bucket ARN rather than on my-bucket/*, or you are relying on ACLs while Object Ownership is set to Bucket owner enforced, which disables them. Prefer presigned URLs over public buckets when only some people need access.

How do I find out which policy is denying my S3 request?

Read the full error message: since 2021 AWS names the policy type that denied the request, such as a service control policy or a resource-based policy. Then use the IAM Policy Simulator to test the exact action and resource, check CloudTrail for the event's errorMessage, and use IAM Access Analyzer to validate the policy text.

What is the difference between AccessDenied, InvalidAccessKeyId and SignatureDoesNotMatch?

InvalidAccessKeyId means the access key ID does not exist (deleted, mistyped or from another account). SignatureDoesNotMatch means the key exists but the secret is wrong, or the request was signed for a different region or with a skewed clock. AccessDenied means both were right and a policy refused the action. S3 Viewer shows the first two as “The server rejected these credentials” and the last as “The server credentials don't allow this”.

Why do I get Access Denied on Cloudflare R2?

R2 API tokens carry their own permissions: an Object Read only token cannot write, and a token scoped to specific buckets cannot call ListBuckets at all. Check the token's permission level and bucket scope in the R2 dashboard. When a bucket-scoped token connects to S3 Viewer, it asks you to type the bucket names instead of failing.
Use S3 Viewer for this

Skip the CLI. Try it in the browser.

S3 Viewer turns the steps above into a single click. Open source, self-hostable, free.