AWS

An error occurred (AccessDenied) when calling the GetObject operation

S3 denies by default and four separate policies can each veto. How to work out which one said no, using the IAM policy simulator rather than guessing.

medium fix6 min read

the aws error
An error occurred (AccessDenied) when calling the GetObject operation: Access Denied

An error occurred (AccessDenied) when calling the PutObject operation: User: arn:aws:sts::123456789012:assumed-role/app-role/i-0abc is not authorized to perform: s3:PutObject on resource: "arn:aws:s3:::acme-assets/logs/app.log"

<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Confirm which identity is actually making the call

    aws sts get-caller-identity

    The Arn here is what must be allowed. On EC2 or ECS it is an assumed-role ARN, not the role ARN, and a policy written against the wrong one of those denies silently.

  2. 2

    Ask IAM whether that identity is allowed, instead of reading policies by eye

    aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::123456789012:role/app-role --action-names s3:GetObject --resource-arns arn:aws:s3:::acme-assets/logs/app.log

    The simulator evaluates identity policies, and reports allowed or implicitDeny or explicitDeny. An explicitDeny means something is actively denying, which narrows it to an SCP, a permissions boundary, or a Deny statement.

  3. 3

    Check the bucket policy and Block Public Access separately

    aws s3api get-bucket-policy --bucket acme-assets --query Policy --output text | python3 -m json.tool

    A bucket policy can deny even when IAM allows. Block Public Access also overrides any policy that would grant public access, and it is enabled by default on new buckets.

All 10 sections

S3 denies by default, and four independent mechanisms can each block a request. An allow in one does not override a deny in another, so the question is always which one said no.

MechanismCan it deny?
IAM identity policyYes, by absence or explicit Deny
Bucket policyYes
Block Public AccessYes, overrides policies granting public access
SCP or permissions boundaryYes, explicit Deny wins over everything
KMS key policyYes, for encrypted objects

Start with who you actually are

aws sts get-caller-identity
{
  "UserId": "AROA...:i-0abc123",
  "Account": "123456789012",
  "Arn": "arn:aws:sts::123456789012:assumed-role/app-role/i-0abc123"
}

Note the ARN is sts::...:assumed-role/app-role/..., not iam::...:role/app-role. A bucket policy Principal written as the assumed-role ARN with the session name will not match reliably. Use the role ARN in policies and the assumed-role ARN only when you know why.

Let IAM answer instead of reading policies

The simulator evaluates the real policy set:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/app-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::acme-assets/logs/app.log
"EvalDecision": "implicitDeny"
DecisionMeaning
allowedIdentity policy permits it. Look at bucket policy, BPA or KMS
implicitDenyNothing grants it. Add the permission
explicitDenySomething actively denies. SCP, boundary, or a Deny statement

This is far faster than reading four documents and simulating the logic in your head.

The resource ARN trap

The single most common IAM mistake with S3:

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::acme-assets"
}

That grants nothing. Object actions need the object ARN with a key path; bucket actions need the bucket ARN. They are different resources:

{
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::acme-assets/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": "arn:aws:s3:::acme-assets"
    }
  ]
}

s3:ListBucket on the bucket is needed for aws s3 ls and for aws s3 sync. Without it, sync fails in a way that looks like an object permission problem.

ListBucket also changes the error you get

If you have s3:GetObject but not s3:ListBucket, a request for a missing key returns AccessDenied rather than NoSuchKey. S3 does this deliberately so that a caller cannot probe which keys exist. So an AccessDenied on a path you believe should work may simply mean the object is not there.

Granting s3:ListBucket makes the real error appear.

Bucket policies

aws s3api get-bucket-policy --bucket acme-assets --query Policy --output text | python3 -m json.tool

Look for "Effect": "Deny". A common one enforcing TLS:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": ["arn:aws:s3:::acme-assets", "arn:aws:s3:::acme-assets/*"],
  "Condition": { "Bool": { "aws:SecureTransport": "false" } }
}

That denies plain HTTP. An SDK configured with an HTTP endpoint hits it and reports a generic Access Denied.

Another frequent one requires a specific encryption header on upload:

"Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" } }

Your PutObject then has to send that header.

Block Public Access

aws s3api get-public-access-block --bucket acme-assets

All four settings default to true on buckets created since 2023. BPA overrides any policy or ACL that would grant public access, so a bucket policy with "Principal": "*" is simply ignored.

If you want public reads, CloudFront with Origin Access Control is the better pattern than disabling BPA: the bucket stays private and only CloudFront can read it.

KMS-encrypted objects

An object encrypted with a customer-managed key needs both S3 and KMS permission:

{
  "Effect": "Allow",
  "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
  "Resource": "arn:aws:kms:eu-west-1:123456789012:key/1234abcd-..."
}

And the key policy must allow the principal too. A KMS key policy is not optional the way a bucket policy is: without a grant there, IAM permissions alone are not enough.

This is the cause when GetObject is denied on one bucket while working on another with identical IAM.

Cross-account

Both sides must allow it. The bucket policy grants the other account's role, and that role's IAM policy must also permit the action.

Also set object ownership, or objects written by the other account stay owned by it and your own account cannot read them:

aws s3api put-bucket-ownership-controls --bucket acme-assets \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

BucketOwnerEnforced disables ACLs entirely and makes the bucket owner own everything, which removes a whole class of confusing cross-account failures.

A checklist

  1. aws sts get-caller-identity. Policies must match this identity.
  2. aws iam simulate-principal-policy before reading any policy by hand.
  3. implicitDeny → the identity policy is missing the permission.
  4. Object actions need arn:aws:s3:::bucket/*; bucket actions need arn:aws:s3:::bucket.
  5. Add s3:ListBucket, or a missing key reports AccessDenied instead of NoSuchKey.
  6. Check the bucket policy for Deny on aws:SecureTransport or an encryption header.
  7. Public access → Block Public Access overrides policies. Prefer CloudFront with OAC.
  8. Encrypted objects → KMS permission in IAM and in the key policy.

Frequently Asked Questions

Why does S3 return AccessDenied instead of NoSuchKey for a missing object?

Because without s3:ListBucket permission, telling you the object does not exist would leak information about the bucket's contents. S3 therefore returns AccessDenied for both a forbidden object and a missing one, so an unauthorised caller cannot probe which keys exist. Granting s3:ListBucket on the bucket ARN changes the behaviour and you start getting the real NoSuchKey error, which is worth doing in any environment where you are debugging paths.

What is the difference between a bucket policy and an IAM policy?

An IAM policy is attached to an identity and says what that identity may do. A bucket policy is attached to the bucket and says who may do what to it. Both are evaluated, and a request is allowed only if nothing denies it and at least one of them allows it. For same-account access an IAM policy alone is enough. For cross-account access both are required: the bucket policy must grant the external principal, and that principal's own IAM policy must also permit the action.

Why does my bucket policy granting public access not work?

Block Public Access is almost certainly enabled, and it overrides bucket policies and ACLs that would grant public access. All four of its settings default to on for buckets created since 2023. You can disable it, and a better pattern for serving public content is CloudFront with Origin Access Control: the bucket stays fully private, only the CloudFront distribution can read it, and you gain caching and TLS termination at the edge.

Why is GetObject denied on one bucket but not another with the same IAM policy?

The most likely difference is encryption with a customer-managed KMS key. Objects encrypted that way need kms:Decrypt in the caller's IAM policy and a grant for that principal in the KMS key's own key policy. Unlike a bucket policy, the key policy is not optional: IAM permissions alone cannot grant access to a KMS key that does not allow the principal. Check which key the bucket uses with aws s3api get-bucket-encryption.

How do I debug this without reading four policy documents?

Use aws iam simulate-principal-policy with the role ARN, the action and the exact resource ARN. It evaluates the real identity policies and returns allowed, implicitDeny or explicitDeny, which immediately tells you whether the problem is a missing grant or an active denial from an SCP, permissions boundary or Deny statement. If it returns allowed and the call still fails, you have narrowed the cause to the bucket policy, Block Public Access or KMS.

Reference and practice

Learn the underlying concept

Other AWS errors