LEARN · DEBUGGING GUIDE

AWS S3 403 Access Denied: Debugging Bucket Policy Issues

An S3 403 error often means the bucket policy explicitly denies the request or the requester isn't allowed. This guide shows you how to pinpoint the exact rule blocking access.

IntermediateCloud7 min read

What this usually means

An S3 403 error from bucket policy means the bucket policy was evaluated and either explicitly denied the request (Deny statement) or no Allow statement matched the request context. Unlike IAM policies, bucket policies are resource-based and can grant or deny access to principals outside your account. The tricky part: AWS evaluates both identity-based (IAM) and resource-based (bucket policy) policies. A Deny in either wins. Also, bucket policies can have conditions (IP address, VPC, TLS) that silently block requests. The 403 response doesn't tell you which rule fired—you have to decode the request context and simulate the policy.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `aws s3api get-bucket-policy --bucket <bucket-name>` to check the current policy JSON.
  • 2Use AWS IAM Access Analyzer for S3: navigate to the bucket in Console → Permissions → Access Analyzer for S3 → 'Generate policy' to see effective permissions.
  • 3Examine CloudTrail event for the failed request: look for `errorCode: AccessDenied`, `requestParameters`, and `userIdentity`.
  • 4Test with `aws s3api put-object --bucket <bucket> --key test.txt --body /dev/null --profile <source-profile>` and check the exact error message.
  • 5Simulate the request with `aws s3api put-object-acl --bucket <bucket> --key <key> --acl bucket-owner-full-control` if bucket is configured for Requester Pays.
  • 6Check bucket policy for condition blocks: `aws s3api get-bucket-policy --bucket <bucket> | jq '.Policy | fromjson | .Statement[].Condition'`
  • 7If cross-account, verify the external account has a matching IAM policy allowing s3:PutObject (or whatever action) on the bucket ARN.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchBucket policy JSON: `aws s3api get-bucket-policy --bucket <bucket>`
  • searchCloudTrail event history: `aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject --max-results 10`
  • searchIAM policy attached to the caller: `aws iam list-attached-user-policies --user-name <user>`
  • searchVPC Endpoint policy if using S3 Gateway Endpoint: `aws ec2 describe-vpc-endpoints --vpc-endpoint-ids <vpce-id>`
  • searchS3 access logs (if enabled): `aws s3api get-bucket-logging --bucket <bucket>`
  • searchAWS Config rules for S3 bucket public access: `aws configservice describe-config-rules`
  • searchRequester Pays configuration: `aws s3api get-bucket-request-payment --bucket <bucket>`
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningBucket policy has an explicit Deny statement (e.g., Deny for anything not from a specific VPC).
  • warningBucket policy uses `Principal: "*"` but the action is `s3:*` and the bucket has Block Public Access enabled, overriding the Allow.
  • warningCondition block requires `aws:SourceIp` but the request comes from a VPC or different IP range.
  • warningCross-account request: bucket policy allows the external account but that account's IAM user lacks the action on the bucket ARN (missing `Resource: arn:aws:s3:::bucket/*`).
  • warningBucket policy uses `aws:SourceArn` or `aws:SourceAccount` and the request's source doesn't match (e.g., CloudFront OAI not set correctly).
  • warningObject ACL is set to 'bucket-owner-full-control' but the bucket policy only grants access to the bucket owner's account.
  • warningRequester Pays bucket: request must include `x-amz-request-payer: requester` header.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAdd an explicit Allow statement for the principal and action, ensuring no conflicting Deny exists.
  • buildRemove or adjust condition blocks (e.g., remove IP restriction or update to correct VPC endpoint).
  • buildFor cross-account: ensure the external account's IAM policy includes the same action on the bucket ARN (e.g., `arn:aws:s3:::bucket-name/*`).
  • buildDisable Block Public Access settings if the bucket policy intends to allow public access (not recommended for sensitive data).
  • buildSet object ACL to `bucket-owner-full-control` if the bucket policy expects the bucket owner to always have access.
  • buildIf using Requester Pays, add `--request-payer requester` to CLI commands or `x-amz-request-payer: requester` in SDK.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun the same command that failed and expect HTTP 200.
  • verifiedUse `aws s3api get-object --bucket <bucket> --key <key> /dev/null` with the same principal and context.
  • verifiedCheck CloudTrail for a successful event (no errorCode) after the fix.
  • verifiedUse the IAM Policy Simulator: https://policysim.aws.amazon.com/ with the bucket policy and principal ARN.
  • verifiedTest with different conditions (e.g., from a different IP or VPC) to ensure condition-based policies work.
  • verifiedIf changed bucket policy, run `aws s3api get-bucket-policy --bucket <bucket>` to confirm the update.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningNot reading the full error message: sometimes it says 'Access Denied' but the body contains 'Authorization header missing' or 'Requests must be signed'.
  • warningAssuming IAM permissions are sufficient: bucket policies can override IAM with explicit Deny.
  • warningOverlooking Block Public Access settings: these apply even if the bucket policy allows public access.
  • warningForgetting that S3 evaluates all policies: identity-based, resource-based, and any Service Control Policies.
  • warningTesting with a different principal than the failing one (e.g., using root account when the app uses an IAM role).
  • warningNot checking object ACLs: if object ACL is 'private', the bucket owner (different account) might be denied access even if bucket policy allows.
( 07 )War story

Cross-account S3 upload fails with 403 despite bucket policy allowing external account

DevOps EngineerPython 3.9, boto3 1.26, AWS Lambda (Node.js 18), S3

Timeline

  1. 10:15Data pipeline team reports 403 error when Lambda in Account B tries to write objects to our S3 bucket in Account A.
  2. 10:20I check bucket policy: Allow s3:PutObject for Principal: 'arn:aws:iam::ACCOUNT-B:root' on arn:aws:s3:::our-bucket/*. Looks correct.
  3. 10:25Check IAM role in Account B: attached policy allows s3:PutObject on arn:aws:s3:::our-bucket/*. Also looks correct.
  4. 10:30I test manually: `aws s3 cp test.txt s3://our-bucket/ --profile account-b-dev` — it works! So why does Lambda fail?
  5. 10:35CloudTrail event for Lambda failure shows userIdentity: 'AROAEXAMPLE:my-lambda-session' (assumed role). That's fine.
  6. 10:40Check bucket policy again: Principal is account root ("AWS": "arn:aws:iam::ACCOUNT-B:root") — this grants access to all IAM users/roles in Account B. Should be okay.
  7. 10:45Realize Lambda is in a VPC with VPC Endpoint for S3. The bucket policy might have a condition. Indeed: `"Condition": {"StringNotEquals": {"aws:SourceVpce": "vpce-123"}}` (Deny if not from that VPCE). Lambda's VPC is using a different VPCE. Duh.
  8. 10:50Update bucket policy: remove the VPCE condition, or add the Lambda's VPCE to the condition. Test again — works.

I got paged at 10:15 AM Friday. The data pipeline team was frantic: their Lambda in Account B couldn't write to our S3 bucket. The error was a plain 'Access Denied' with no additional hint. I jumped onto the AWS Console.

First, I verified the bucket policy. It had an Allow for the entire Account B root principal. That should cover any IAM role in that account. I checked the Lambda's execution role policy — it had s3:PutObject on our bucket ARN. I even tested with my own credentials from Account B and it worked. So why did the Lambda fail? Classic 'works on my machine' trap.

I dived into CloudTrail. The event showed the Lambda's assumed role ARN and the error. The request seemed fine. Then I noticed the Lambda was in a VPC with an S3 VPC Endpoint. I checked the bucket policy again — there was a condition that restricted access to a specific VPC Endpoint ID (vpce-123). The Lambda's VPC used vpce-456. That Deny condition was silently blocking the request. I updated the policy to allow both VPCEs. Fixed.

Root cause

Bucket policy had a condition denying requests not from a specific VPC Endpoint. The Lambda's VPC used a different endpoint.

The fix

Modified the bucket policy condition to include the Lambda's VPC Endpoint ID, or removed the condition entirely (as the intent was to allow cross-account access).

The lesson

Always check condition blocks in bucket policies, especially when requests come from VPCs. Explicit Deny conditions can override Allow statements.

( 08 )Policy Evaluation Logic for S3

When an S3 request arrives, AWS evaluates all applicable policies: identity-based (IAM user/role policies), resource-based (bucket policy, object ACL), and organization policies (SCPs, VPC endpoint policies). The default implicit deny is overridden only if an explicit Allow matches. An explicit Deny in any policy overrides any Allow.

For 403 errors, the most common scenario is a bucket policy with an Allow but also an explicit Deny (or a condition that effectively denies). Use the IAM Policy Simulator with the exact request context (principal, action, resource, conditions) to see which statement matches.

( 09 )Cross-Account Access: The Double Permission Requirement

For cross-account access, both the resource-based policy (bucket policy) and the identity-based policy (IAM in the other account) must grant the action. The bucket policy allows the external account's root principal, but the external account's IAM user/role must also have an IAM policy allowing s3:PutObject on the bucket ARN.

A common mistake: the bucket policy says `Principal: "arn:aws:iam::OTHER-ACCOUNT:root"` but the IAM role in OTHER-ACCOUNT has a policy with `Resource: "arn:aws:s3:::bucket-name"` (missing the trailing `/*`). S3 requires the object-level resource ARN for PutObject. Always use `arn:aws:s3:::bucket-name/*` for object operations.

( 10 )The Silent Killers: Conditions and Block Public Access

Bucket policy conditions can deny requests based on IP, VPC, TLS version, or time. For instance, a condition `StringNotEquals: aws:SourceVpce` denies any request not coming from the specified VPC Endpoint. If the request is from an EC2 instance in a different VPC, it gets 403 — even if the policy otherwise allows it.

Block Public Access settings override any bucket policy that grants public access. If the bucket has 'Block public access' enabled, a policy with `Principal: "*"` will still fail with 403. This is a common pitfall when trying to set up static website hosting.

( 11 )Debugging with CloudTrail and Access Logs

CloudTrail records every S3 API call. Look for `errorCode: AccessDenied` and check the `requestParameters` to see the exact bucket, key, and headers. The `userIdentity` field shows the principal ARN — ensure it matches what you expect.

S3 server access logs provide more granular info including HTTP status codes, request IDs, and timestamps. Enable logging to a separate bucket and parse with Athena for bulk analysis. The log format includes the requester, bucket, key, and the operation.

Frequently asked questions

Why does my presigned URL return 403 even though I have permissions?

A presigned URL inherits the permissions of the user who generated it at the time of generation. If the bucket policy later denies access (e.g., due to a condition), the URL will fail. Also, check that the URL isn't expired and that the action matches what the presigned URL was signed for (e.g., PutObject vs GetObject).

Can a bucket policy grant access to an anonymous user?

Yes, set `Principal: "*"` and allow the action. However, you must also disable 'Block public access' settings at the bucket or account level. Otherwise, the request will be denied even if the policy allows it.

What is the difference between bucket policy and IAM policy for S3?

Bucket policy is attached to the bucket and controls who can access that bucket (resource-based). IAM policy is attached to a user/role and controls what that entity can do (identity-based). For cross-account access, both are required. For same-account, either can be used, but bucket policy can include conditions that IAM cannot (like VPC endpoint).

How do I test bucket policy changes without affecting production?

Use the IAM Policy Simulator: paste your bucket policy and specify the principal, action, and conditions. You can simulate from a different account by using the 'Switch Role' feature. Alternatively, create a test bucket with a copy of the policy and test against it.