← Back to Blog

AWS S3 Access Denied: Fix 403 Forbidden Errors Systematically

You run aws s3 cp and get "Access Denied." You check the bucket policy. It looks fine. You check your IAM policy. That looks fine too. And yet S3 still says no. This is one of the most frustrating errors in AWS because there are at least six different places where access can be blocked. Let us walk through every single one of them.

TL;DR

S3 access requires both an IAM policy and a bucket policy to allow the action. There is no single place to fix it. Start with these three commands:

  • aws sts get-caller-identity - confirms who you actually are
  • aws s3api get-bucket-policy --bucket BUCKET - shows the bucket policy
  • aws s3api get-public-access-block --bucket BUCKET - shows Block Public Access settings

If any one of IAM policy, bucket policy, Block Public Access, ACLs, KMS key policy, VPC endpoint policy, or SCPs says "deny," the request is denied. Period.

Why S3 Returns 403 for Everything (And That Is Intentional)

Here is something that trips up a lot of people. When you try to access an S3 object that does not exist, you might expect a 404 Not Found. But instead you get 403 Access Denied. What gives?

This is a deliberate security decision by AWS. If S3 returned 404 for nonexistent objects, an attacker could enumerate your bucket contents by checking which keys return 404 vs 403. By returning 403 for everything you cannot access, S3 prevents information leakage about what actually exists in the bucket.

The practical consequence is that when you see "Access Denied," you cannot immediately tell whether the problem is permissions or whether the object simply does not exist. That is why a systematic approach matters. You need to check each layer of the S3 access control model one by one.

The other thing worth knowing is that S3 does not distinguish between authentication failures and authorization failures. Both come back as 403. There is no 401 Unauthorized response from S3. Your credentials could be completely wrong, or your credentials could be valid but lack the right permissions, and the error message looks the same either way.

Step 1: Identify Who You Are

Before you debug anything else, confirm exactly which identity is making the request. This sounds obvious, but it catches people more often than you would expect. You might think you are using your admin user, but your terminal session has an expired token from an assumed role. Or your EC2 instance might be using a different instance profile than you think.

aws sts get-caller-identity

This returns three things: the account ID, the ARN of the principal, and the user ID. Pay close attention to the ARN. If you see assumed-role/SomeRole/session-name, then you are operating under that role, not your IAM user. Every policy you check from this point forward needs to apply to that specific ARN.

{
    "UserId": "AROA3XFRBF23EXAMPLE:my-session",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/MyS3Role/my-session"
}

If the account ID is different from the one that owns the bucket, you are dealing with cross-account access, which has its own set of requirements. We will cover that in Step 7.

Also check whether your credentials have expired. If you are using temporary credentials from STS, they have a maximum lifetime (typically 1 hour for assumed roles). An expired token gives the same 403 error.

# Check if your credentials are still valid
aws sts get-caller-identity 2>&1 | grep -i "expired"

Step 2: Check the Bucket Policy

The bucket policy is the first place most people look, and for good reason. It is the most common source of S3 access issues. Pull up the current policy:

aws s3api get-bucket-policy --bucket my-bucket --output text | python3 -m json.tool

If this command itself returns "Access Denied," that tells you something important: you do not even have s3:GetBucketPolicy permission. You will need to check from the AWS Console or ask someone with admin access.

When reviewing the policy, look for these things in order:

  1. Explicit Deny statements. These always win. If any statement in the policy has "Effect": "Deny" and matches your request, you are blocked regardless of any Allow statements. This is the most common "gotcha" in S3 policies.
  2. Allow statements for your principal. Check whether the Principal field includes your IAM user ARN, role ARN, or account ID. A "Principal": "*" allows everyone (subject to Block Public Access).
  3. Resource ARN format. This is a classic mistake. S3 has two resource types: the bucket itself (arn:aws:s3:::my-bucket) and objects within the bucket (arn:aws:s3:::my-bucket/*). Bucket-level operations like ListBucket need the bucket ARN. Object-level operations like GetObject and PutObject need the object ARN with /*. Missing the /* is the single most common bucket policy error I see.
  4. Condition keys. Look for conditions like aws:SourceIp, aws:SourceVpce, or aws:PrincipalOrgID that might be restricting access based on your network location or organization.

Here is an example of a bucket policy that looks right but is subtly broken:

{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::123456789012:role/MyRole"},
        "Action": ["s3:GetObject", "s3:PutObject"],
        "Resource": "arn:aws:s3:::my-bucket"
    }]
}

See the problem? The Resource is arn:aws:s3:::my-bucket instead of arn:aws:s3:::my-bucket/*. GetObject and PutObject are object-level actions, so they need the object ARN. This policy allows nothing useful.

Step 3: Check the IAM Policy

Even if the bucket policy allows your principal, the IAM policy attached to your user or role must also allow the action. In the same account, S3 uses a union of bucket policy and IAM policy for non-public access. But if either one has an explicit deny, that deny wins.

The fastest way to test your IAM permissions is the IAM Policy Simulator:

  1. Go to the IAM Policy Simulator in the AWS Console.
  2. Select the user or role you identified in Step 1.
  3. Choose the S3 service and the specific action (e.g., s3:GetObject).
  4. Enter the resource ARN (e.g., arn:aws:s3:::my-bucket/*).
  5. Run the simulation. It will tell you whether the action is allowed or denied, and which policy is responsible.

Alternatively, use the CLI to list attached policies and check them manually:

# For an IAM user
aws iam list-attached-user-policies --user-name myuser
aws iam list-user-policies --user-name myuser

# For an IAM role
aws iam list-attached-role-policies --role-name MyRole
aws iam list-role-policies --role-name MyRole

Common S3 actions you might need in your IAM policy:

  • s3:GetObject - download objects
  • s3:PutObject - upload objects
  • s3:DeleteObject - delete objects
  • s3:ListBucket - list objects in a bucket (note: this is a bucket-level action, not object-level)
  • s3:GetBucketLocation - often needed for tools and SDKs to determine the bucket region
  • s3:GetBucketPolicy - read the bucket policy

Step 4: Block Public Access Settings

This one catches even experienced AWS engineers. Block Public Access (BPA) is an account-level and bucket-level setting that overrides bucket policies and ACLs. It was introduced to prevent accidental public exposure, and it is very good at its job.

Check both levels:

# Bucket-level BPA
aws s3api get-public-access-block --bucket my-bucket

# Account-level BPA
aws s3control get-public-access-block --account-id 123456789012

There are four BPA settings, and each does something different:

  • BlockPublicAcls - rejects PUT requests that include public ACLs
  • IgnorePublicAcls - ignores existing public ACLs on the bucket and its objects
  • BlockPublicPolicy - rejects bucket policies that grant public access
  • RestrictPublicBuckets - restricts access to the bucket to only AWS service principals and authorized users

The important thing to understand is that BPA does not just apply to anonymous access. The RestrictPublicBuckets setting will block cross-account access if the bucket policy uses a wildcard principal. And account-level BPA overrides bucket-level settings, so even if BPA is off on the bucket, it can still be on at the account level.

If you are trying to grant cross-account access or set up a public bucket (for static website hosting, for example), BPA is almost certainly what is blocking you. New AWS accounts have BPA enabled at the account level by default since April 2023.

Step 5: Object Ownership and ACLs

S3 Access Control Lists are a legacy access mechanism that still trips people up, especially in cross-account scenarios.

First, check the bucket ownership setting:

aws s3api get-bucket-ownership-controls --bucket my-bucket

There are three possible values:

  • BucketOwnerEnforced - ACLs are disabled entirely. The bucket owner owns all objects regardless of who uploaded them. This is the default for new buckets and the recommended setting.
  • BucketOwnerPreferred - objects written with the bucket-owner-full-control ACL are owned by the bucket owner.
  • ObjectWriter - the uploading account owns the object. This can cause cross-account access issues where the bucket owner cannot access objects uploaded by another account.

If the ownership is set to ObjectWriter and objects were uploaded by a different account without the bucket-owner-full-control ACL, the bucket owner gets 403 when trying to access those objects. This is a very common and very confusing scenario.

To check an object's ACL:

aws s3api get-object-acl --bucket my-bucket --key my-object.txt

The fix for ACL issues is usually to switch the bucket to BucketOwnerEnforced and re-upload the objects, or to copy the objects in place to reset ownership:

# Copy the object to itself to change ownership
aws s3 cp s3://my-bucket/my-object.txt s3://my-bucket/my-object.txt \
  --metadata-directive REPLACE

Step 6: KMS Encryption

If your bucket uses SSE-KMS encryption (server-side encryption with AWS KMS keys), there is an additional layer of permissions you need to worry about. It is not enough to have S3 permissions. You also need KMS permissions.

Check the bucket's default encryption:

aws s3api get-bucket-encryption --bucket my-bucket

If the output shows "SSEAlgorithm": "aws:kms", then every GetObject request also requires kms:Decrypt permission, and every PutObject request requires kms:GenerateDataKey permission.

These permissions need to exist in two places:

  1. Your IAM policy must include kms:Decrypt and/or kms:GenerateDataKey for the KMS key ARN.
  2. The KMS key policy must allow your principal to use the key.

This is the command to check the KMS key policy:

# First, get the KMS key ID from the bucket encryption config
# Then check the key policy
aws kms get-key-policy --key-id KEY_ID --policy-name default --output text

A common mistake is using the AWS managed S3 key (aws/s3) for cross-account access. The AWS managed key cannot be shared across accounts. If you need cross-account access to KMS-encrypted objects, you must use a customer-managed KMS key and explicitly grant the external account access in the key policy.

Step 7: Cross-Account Access

Cross-account S3 access is where all of these layers compound, and it is the source of some truly maddening 403 errors. Here is the deal: when Account A tries to access a bucket in Account B, permissions must be granted on both sides.

In Account B (bucket owner): The bucket policy must explicitly allow the principal from Account A.

{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::111111111111:role/CrossAccountRole"},
        "Action": ["s3:GetObject", "s3:ListBucket"],
        "Resource": [
            "arn:aws:s3:::my-bucket",
            "arn:aws:s3:::my-bucket/*"
        ]
    }]
}

In Account A (requester): The IAM policy attached to the role or user must also allow the S3 actions on that bucket.

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

Both policies are required. If either side is missing, you get 403. And remember the BPA check from Step 4. If Block Public Access is enabled and the bucket policy uses a broad principal like "Principal": "*", BPA will block the cross-account access even though the policy technically allows it.

For KMS-encrypted buckets in cross-account scenarios, you also need the KMS key policy in Account B to grant kms:Decrypt to Account A's principal. That is three policies that all need to agree.

Step 8: VPC Endpoint Policy

If your application runs inside a VPC and uses an S3 VPC Gateway Endpoint, there is one more policy to check. The VPC endpoint itself can have a policy that restricts which S3 buckets and actions are allowed through it.

# List VPC endpoints
aws ec2 describe-vpc-endpoints --filters "Name=service-name,Values=com.amazonaws.us-east-1.s3"

# Check the endpoint policy
aws ec2 describe-vpc-endpoints --vpc-endpoint-ids vpce-1234567890abcdef0 \
  --query 'VpcEndpoints[0].PolicyDocument' --output text

The default VPC endpoint policy allows full access to all S3 buckets, but if someone has customized it to restrict access to specific buckets, any bucket not in the policy will return 403.

You can also be affected by the inverse scenario: a bucket policy with a condition like "aws:sourceVpce": "vpce-1234567890abcdef0" will only allow access from that specific VPC endpoint and deny everything else, including access from your laptop.

Pro Tip: Use aws s3api head-object --bucket my-bucket --key my-object.txt instead of aws s3 cp when troubleshooting. The head-object command gives a more specific error and avoids actually transferring the file. It helps you narrow down whether the issue is with GetObject, ListBucket, or something else entirely.

S3 Policy Types Comparison

Here is a quick reference of every policy type that can affect S3 access:

Policy Type Scope Can Deny? Common Mistake
IAM Policy Attached to user/role/group Yes Wrong Resource ARN (missing /* for object actions)
Bucket Policy Attached to the S3 bucket Yes Explicit Deny overriding all Allow statements
ACL Per bucket or per object No (allow-only) ObjectWriter ownership causing cross-account issues
Block Public Access Account or bucket level Yes (override) Account-level BPA blocking bucket-level policy
VPC Endpoint Policy Per VPC endpoint Yes Custom policy not including the target bucket
SCP (Organizations) AWS Organization OU/account Yes SCP restricting S3 actions org-wide without exception

Six Common Mistakes That Cause S3 403 Errors

After troubleshooting hundreds of S3 access issues, these are the mistakes I see most often:

  1. Forgetting s3:ListBucket for "no such key" errors. Without s3:ListBucket permission, S3 returns 403 instead of 404 when an object does not exist. This is the most confusing behavior in S3. You think you have a permissions problem when the object simply is not there. Add s3:ListBucket on the bucket ARN (not the object ARN) to see real 404 responses.
  2. Wrong Resource ARN format. Bucket-level actions (ListBucket, GetBucketLocation) need arn:aws:s3:::bucket-name. Object-level actions (GetObject, PutObject) need arn:aws:s3:::bucket-name/*. You almost always need both in the same policy.
  3. Block Public Access silently blocking everything. BPA is enabled by default on new accounts. If you are setting up a public bucket or cross-account access with a wildcard principal, BPA will block it with no clear error message beyond "Access Denied." Always check both account-level and bucket-level BPA.
  4. Using aws/s3 managed KMS key for cross-account access. The AWS managed S3 key cannot be shared across accounts. You need a customer-managed CMK with an explicit key policy granting the external account access.
  5. Expired temporary credentials. If you assumed a role or are using SSO, your credentials might have expired. The error is the same 403. Run aws sts get-caller-identity to verify. If it fails, your credentials are expired.
  6. S3 bucket in the wrong region. While most S3 operations are globally routed, some older configurations and certain API calls require the correct region. If you get "PermanentRedirect" alongside access issues, try specifying the region: aws s3api get-object --region us-west-2 --bucket my-bucket --key file.txt outfile

Check Your Cloud Exposure

S3 misconfigurations are one of the top causes of data breaches. Use SecureBin Exposure Checker to scan your domain for exposed files, misconfigured headers, and other security issues.

Scan Your Domain Free

A Systematic Debugging Checklist

When you hit an S3 403, work through these steps in order. Do not skip ahead. Each layer can independently block your access, and fixing one layer does not help if another is still blocking.

  1. Run aws sts get-caller-identity and confirm the principal ARN and account ID.
  2. Check the bucket policy for explicit denies matching your principal.
  3. Check your IAM policy for the required S3 actions and correct resource ARNs.
  4. Check Block Public Access at both bucket and account level.
  5. Check object ownership and ACLs if the objects were uploaded by a different account.
  6. Check KMS key policy if the bucket uses SSE-KMS encryption.
  7. If cross-account, verify that both the bucket policy and the external IAM policy allow the access.
  8. If inside a VPC, check the VPC endpoint policy.
  9. If using AWS Organizations, check Service Control Policies.

If all of those check out and you are still getting 403, enable AWS CloudTrail and look at the event for your failed request. The errorCode and errorMessage fields in the CloudTrail event will tell you exactly which policy evaluation failed. That is your definitive answer.

# Search CloudTrail for recent S3 access denied events
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
  --max-results 5 \
  --query 'Events[?contains(CloudTrailEvent, `AccessDenied`)].CloudTrailEvent' \
  --output text

Frequently Asked Questions

Why does S3 return 403 instead of 404 when an object does not exist?

S3 returns 403 Access Denied instead of 404 Not Found when you do not have s3:ListBucket permission on the bucket. This is an intentional security design. Without ListBucket permission, S3 refuses to confirm whether an object exists or not, because even revealing that information could be a data leak. If you add s3:ListBucket permission (on the bucket ARN, not the object ARN), S3 will return the expected 404 for missing objects.

How do I troubleshoot S3 access denied errors for cross-account access?

Cross-account S3 access requires permissions on both sides. First, the bucket policy in the owning account must explicitly grant access to the external account or role ARN. Second, the IAM policy in the requesting account must allow the S3 actions on that bucket. Neither side alone is sufficient. Also check that Block Public Access settings are not interfering, and if the objects are KMS-encrypted, the KMS key policy must also grant the external account access. For a related walkthrough on IAM issues, see our IAM Access Denied Troubleshooting Guide.

Does Block Public Access override my bucket policy?

Yes. Block Public Access (BPA) acts as a hard override on top of bucket policies and ACLs. If BPA is enabled at the account level or bucket level, it will block public access even if your bucket policy explicitly allows it. BPA does not affect access from authenticated IAM principals within the same account, but it will block any policy statement that grants access to the wildcard principal (*). Always check both account-level and bucket-level BPA settings.

Secure Your AWS Environment

S3 access issues often point to deeper configuration problems. Run a free scan with SecureBin Exposure Checker to find exposed resources, misconfigured security settings, and potential data leaks.

Run a Free Security Scan

The Bottom Line

S3 403 errors are frustrating because there is no single place to look. IAM policies, bucket policies, Block Public Access, ACLs, KMS key policies, VPC endpoint policies, and SCPs can all independently block your access. The systematic approach, checking each layer in sequence and using tools like get-caller-identity, the IAM Policy Simulator, and CloudTrail, will get you to the root cause every time.

The most common fixes are adding /* to the resource ARN for object-level actions, adding s3:ListBucket to eliminate false 403s on missing objects, and disabling Block Public Access when you intentionally need public or cross-account access.

For a broader look at securing your AWS account, check out our AWS Security Checklist for Production.

Related tools: Exposure Checker, SSL Checker, DNS Lookup, JWT Decoder, and 70+ more free tools.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.