Expected Behavior and the 403 Access Denied Error

When interacting with Amazon S3 (Simple Storage Service)—whether downloading a file via a web browser, uploading artifacts via the AWS CLI, or reading configuration files via an application running on EC2—the expected behavior is a fast, seamless transaction yielding a 200 OK HTTP status.

However, one of the most frustrating and frequent roadblocks cloud administrators face is the 403 Access Denied error or AccessDenied exception.

Because AWS enforces a zero-trust model by default (all access is denied unless explicitly allowed), any missing permission in the chain of authorization will result in a 403. Furthermore, an explicit Deny anywhere in your policies will completely override any Allow.

Prerequisites

Before troubleshooting the issue, gather the following context:

  • The exact name of the S3 Bucket.
  • The IAM User, IAM Role (if using EC2/EKS/Lambda), or AWS Access Keys being used for the request.
  • Whether the request is coming from the same AWS account or a cross-account connection.
  • Whether the objects are KMS-encrypted.

Root Causes of S3 403 Access Denied

A 403 Access Denied error in S3 typically boils down to one of these overlapping security layers:

  1. Missing IAM Permissions: The IAM user or role attempting the action lacks s3:GetObject (to download), s3:PutObject (to upload), or s3:ListBucket.
  2. Bucket Policy Conflicts: The S3 bucket has a .json Bucket Policy that lacks a resource statement or contains an explicit Effect: Deny for the user/IP address.
  3. Block Public Access is Enabled: If you are trying to make a bucket public for website hosting, the global “Block Public Access” setting will reject public requests, regardless of what the Bucket Policy says.
  4. KMS Decryption Errors: If the bucket leverages AWS KMS (Key Management Service) for encryption, the IAM user needs s3:GetObject and kms:Decrypt. If the KMS key permission is missing, S3 throws a 403.
  5. Object Ownership vs Bucket Ownership: In cross-account scenarios, Account A uploads a file to Account B’s bucket. Account B attempts to read it and gets a 403 because Account A implicitly owns the object.

Step-by-Step Solution

1. Identify the Exact Permitted Action Needed

First, determine what the application or user is trying to do. If you are uploading a file via CLI:

aws s3 cp file.txt s3://my-cloud-bucket/

This requires s3:PutObject. If you are trying to view the contents of the bucket via aws s3 ls, this requires s3:ListBucket.

2. Verify IAM User/Role Permissions

Navigate to the IAM Console and find the User or Role attached to your system. They must have a policy granting access to the exact ARN of the bucket.

A correct IAM policy allowing read/write looks like this:

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

Crucial Note: Resource arn:aws:s3:::my-bucket applies to the bucket itself (for ListBucket). Resource arn:aws:s3:::my-bucket/* applies to the objects inside the bucket (for GetObject/PutObject). Mixing these up is a primary cause of 403s!

3. Check the S3 Bucket Policy

Navigate to the S3 Console, select your bucket, and click the Permissions tab. Scroll down to Bucket Policy.

Even if your IAM User has full Admin privileges, an explicit Deny in the Bucket Policy will override it. For example, if you see a policy meant to restrict access to a specific VPN IP:

{
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:*",
    "Resource": "arn:aws:s3:::my-cloud-bucket/*",
    "Condition": {
        "NotIpAddress": {"aws:SourceIp": "203.0.113.0/24"}
    }
}

If you are testing from an IP outside that range, you will receive a 403.

4. Making Objects Public (Web Hosting)

If your goal is to host a static website or public images and you receive a 403:

  1. Go to the Permissions tab in S3.
  2. Edit Block public access (bucket settings) and uncheck “Block all public access”. Save changes.
  3. Add the following Bucket Policy to explicitly allow the internet to read your objects:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-cloud-bucket/*"
        }
    ]
}

5. Check KMS Encryption (Alternative Solution)

If permissions look completely correct, check if the bucket uses Customer Managed KMS keys (SSE-KMS). Go to the Properties tab of the S3 bucket. If Default Encryption is set to use a custom KMS Key, your IAM role must also have permission to decrypt it.

Attach a policy to your IAM Role allowing:

{
    "Effect": "Allow",
    "Action": [
        "kms:Decrypt",
        "kms:GenerateDataKey"
    ],
    "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
}

Prevention

  • Follow Least Privilege: Avoid overly broad s3:* policies. Assign exactly s3:GetObject and s3:PutObject to specific ARN paths.
  • Leverage AWS Policy Simulator: Use the IAM Policy Simulator in the AWS console. You can plug in a user, an S3 action, and a bucket name, and AWS will trace exactly which policy is allowing or denying the request.
  • Enforce Bucket Owner Enforced: For cross-account issues, enable “Bucket Owner Enforced” under the S3 Object Ownership settings. This disables individual ACLs (Access Control Lists) and forces all objects uploaded by anyone to automatically be owned by the bucket owner, vastly simplifying permissions.

Summary

  • 403 Access Denied in S3 is caused by conflicts between IAM Policies, Bucket Policies, and KMS keys.
  • Explicit Deny overrides everything, including administrator access.
  • Ensure ARNs specify /* when granting object-level permissions like GetObject.
  • Disable “Block Public Access” if you need the bucket open to the internet.
  • Confirm the IAM role has kms:Decrypt access if your bucket uses KMS encryption.