S3 Bucket Policies and ACLs

Implement S3 bucket policies and ACLs in this Cloud security essentials lesson. Learn how to control access to your S3 buckets using bucket policies and ACLs, with hands-on steps, comparisons, troubleshooting, and next steps.

Focus: implement s3 bucket policies and acls

Sponsored

You've built a great application, stored your assets in S3, and then — one misconfigured bucket policy later — your entire database backups are publicly downloadable. It's a classic cloud security nightmare, and it happens far more often than you'd think. The pain is real: S3 offers multiple, overlapping access control mechanisms, and getting them wrong can expose sensitive data to the entire internet. This lesson cuts through the confusion, giving you a clear, practical framework to implement S3 bucket policies and ACLs with confidence, so you can lock down your data without breaking your applications.

The problem this lesson solves

S3 access control is a multi-layered beast. You have IAM policies attached to users and roles, bucket policies attached to the bucket itself, and Access Control Lists (ACLs) that are an older, simpler mechanism. When you're starting out, it's tempting to just set a bucket policy that makes everything public "to get it working" — and that's precisely how data breaches happen. The core pain is understanding which mechanism to use when, and how they interact. Getting it wrong can mean either a security hole that exposes sensitive files or a locked-down bucket that breaks your application's functionality. This lesson gives you a decision-making framework and hands-on practice to implement S3 bucket policies and ACLs correctly from the start.

Core concept / mental model

Think of your S3 bucket as a physical filing cabinet. Bucket policies are like a set of rules posted on the side of the cabinet that say who can open it and what they can take. ACLs are like old-fashioned sticky notes on individual folders or files that grant access to specific people (or everyone). IAM policies are the keys you hand to your employees — they define what each person is allowed to do across all the cabinets in the office.

The key principle is the most restrictive permission wins. When a request comes in, AWS evaluates all applicable policies — IAM, bucket, and ACL — and if any of them denies the action, the request is denied. This means you can use bucket policies to set a baseline (e.g., "this bucket is private") and then use ACLs for specific exceptions (e.g., "this one folder is public for a website"). However, the trend in AWS is to move away from ACLs and toward bucket policies and IAM for almost everything, because they are more expressive and easier to audit.

Pro tip: If you're starting a new project, use bucket policies and IAM exclusively. Consider disabling ACLs entirely (by setting the bucket's Object Ownership to Bucket owner enforced). This simplifies your security model and prevents accidental public exposure via ACL misconfiguration.

How it works step by step

Step 1: Understand the Difference

  • Bucket Policies: JSON-based resource policies attached directly to your S3 bucket. They can grant access to other AWS accounts, specific IP ranges, or even anonymous users (for public website hosting). They are the modern standard for bucket-level access control.
  • ACLs: Legacy permission mechanism that uses predefined grants (READ, WRITE, FULL_CONTROL) for specific grantees (the bucket owner, a specific AWS account, or AllUsers/Everyone). They are simpler but less flexible.
  • IAM Policies: Identity-based — attached to IAM users, groups, or roles. They define what that identity can do across all services. They work in conjunction with bucket policies.

Step 2: Plan Your Access Model

Before you write any JSON, ask these questions: 1. Who needs access to the bucket? (e.g., only my application's IAM role, a specific partner AWS account, or the public internet?) 2. What actions should they be able to perform? (e.g., GetObject, PutObject, ListBucket) 3. Are there any conditions? (e.g., only from my office IP range, only over HTTPS, only after a specific date?)

Step 3: Write the Policy

For a bucket policy, you'll create a JSON document that includes an Effect (Allow or Deny), a Principal (who the policy applies to — can be a specific ARN, a wildcard * for everyone, or an AWS account), Action (the S3 API actions), and Resource (the bucket ARN and the ARN of objects within it, e.g., arn:aws:s3:::my-bucket/*).

Step 4: Attach and Test

Apply the policy via the AWS Console, CLI, or Infrastructure-as-Code (e.g., Terraform, CloudFormation). Then test with the aws s3 ls or aws s3 cp commands to verify that the desired access is allowed and everything else is denied.

Hands-on walkthrough

Let's get our hands dirty. We'll use the AWS CLI but the same principles apply in the console or code.

Prerequisites

  • AWS CLI installed and configured with credentials that have permission to modify S3 buckets.
  • An S3 bucket already created (e.g., my-secure-bucket-2024).

Example 1: Private Bucket with Bucket Policy (Allow Only Your IAM Role)

Save the following policy as private-bucket-policy.json:

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

Apply it to your bucket:

aws s3api put-bucket-policy --bucket my-secure-bucket-2024 --policy file://private-bucket-policy.json

# Test: try to list with a different role (should fail)
aws s3 ls s3://my-secure-bucket-2024 --profile other-role

# Expected output (or similar):
# An error occurred (AccessDenied) when calling the ListBucket operation: Access Denied

Example 2: Public Read-only Bucket Policy for a Website

For a static website, you want everyone to be able to read objects, but no one should be able to write. Save as public-read-policy.json:

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

Apply it and test:

aws s3api put-bucket-policy --bucket my-secure-bucket-2024 --policy file://public-read-policy.json

# Upload an object and try to fetch it anonymously (use a curl command)
aws s3 cp index.html s3://my-secure-bucket-2024/
curl https://my-secure-bucket-2024.s3.amazonaws.com/index.html

# Expected output: the HTML content

Example 3: Using ACLs to Grant Read to a Specific AWS Account

First, let's check the current object ownership. If your bucket has Object Ownership set to Bucket owner preferred, you can use ACLs. To grant read access to another account, you'd use aws s3api put-object-acl:

# Grant the bucket owner full control (default) and another account read access
aws s3api put-object-acl \
  --bucket my-secure-bucket-2024 \
  --key data.txt \
  --grant-read 'id=123456789012' \
  --grant-full-control 'id=111122223333'

But ACLs are limited — you can't specify conditions or multiple actions per grant. In modern AWS, you'd do this with a bucket policy instead:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPartnerAccount",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-secure-bucket-2024/*"
    }
  ]
}

Compare options / when to choose what

Feature Bucket Policy ACL IAM Policy
Scope Bucket-level, all objects Object-level or bucket-level Identity-level (user, role)
Conditions (IP, encryption, etc.) Yes (via Condition key) No Yes
Cross-account access Yes Yes (limited) No (must assume role)
Public access Yes (Principal: "*") Yes (via AuthenticatedUsers or AllUsers) No (no anonymous principals)
Best practice Preferred Legacy, disable if possible Use in combination with bucket policies

When to choose what: - Use bucket policies for almost everything — they're flexible, auditable, and support conditions. - Use IAM policies for individual users or roles within your own account. - Avoid ACLs unless you're dealing with legacy buckets or specifically need per-object grants that you can't express in a bucket policy (which is rare).

Troubleshooting & edge cases

1. "Access Denied" even though the policy looks right.

  • Check Object Ownership: If your bucket has Object Ownership set to Bucket owner enforced, ACLs are disabled and any attempt to use put-object-acl will fail with AccessDenied. Similarly, objects uploaded by other accounts might not allow your bucket policy to grant access unless you set the bucket to Bucket owner preferred.
  • Check for an explicit Deny: If your bucket policy has a Deny statement that matches the request, it will override an Allow from elsewhere. Review all policies attached to the bucket and the identity.
  • Check the resource ARN: A common mistake is using arn:aws:s3:::my-bucket without the /* for object actions like s3:GetObject. The bucket ARN only covers bucket-level actions (like s3:ListBucket), while object actions need the wildcard path.

2. Public bucket policy not actually making objects public.

  • Check the S3 Block Public Access settings: If you have Block all public access turned on, your public bucket policy will be ignored (and the put-bucket-policy call might even fail). You must explicitly allow public access for the bucket and account if you intend to host a public website.
  • Check object-level ACLs: If your objects are owned by another account and they don't have ACLs granting read to AllUsers, even a public bucket policy won't help because the object owner controls the object ACL.

3. Unexpected permission for an EC2 instance role.

  • Remember the evaluation logic: All policies are combined. If your IAM role allows s3:* (from a broad IAM policy) and your bucket policy has no explicit deny, the role can access the bucket. To be strict, add a Deny statement in the bucket policy that blocks access from outside your VPC or from specific IPs.

What you learned & what's next

You've now grasped the core idea behind implementing S3 bucket policies and ACLs: you learned how to distinguish them, when to use each, and how to apply them hands-on. You can now write bucket policies that restrict access to specific principals, make a bucket public for a website, and understand why ACLs are legacy. You've also learned the critical troubleshooting steps for common access failures.

Next in the Cloud security essentials path: You'll move on to Amazon GuardDuty: Detecting Threats at Scale, where you'll learn how to continuously monitor your S3 buckets (and other resources) for malicious activity, so you don't just rely on static policies — you'll also have dynamic threat detection.

Practice recap

Try this mini exercise: create a new bucket, upload a test file, and then write a bucket policy that allows read access only to a specific IAM user (use your own ARN). First, test that the policy works, then intentionally add a Deny statement that blocks access from your IP range and verify that you get AccessDenied. This gives you a feel for how policies interact and the importance of explicit Denies.

Common mistakes

  • Using arn:aws:s3:::bucket without /* in the Resource for object-level actions (like s3:GetObject) — this causes the policy to not match any objects.
  • Forgetting to check S3 Block Public Access settings when trying to make a bucket public; the policy might be silently ignored.
  • Ignoring Object Ownership and trying to use ACLs when they are disabled (Bucket owner enforced) — leads to AccessDenied errors.
  • Over-relying on ACLs instead of bucket policies, making it impossible to enforce conditions like IP restrictions or HTTPS.
  • Having a broad IAM policy that grants s3:* to a role, while the bucket policy is restrictive — the unrestricted IAM policy may still allow access.

Variations

  1. Use Infrastructure-as-Code tools like Terraform (aws_s3_bucket_policy resource) or CloudFormation to version-control your bucket policies.
  2. Instead of ACLs, use S3 Object Ownership 'Bucket owner enforced' to disable ACLs completely and rely on bucket policies and IAM for all access control.
  3. For cross-account access, consider using IAM roles and sts:AssumeRole (when the bucket is in another account) instead of bucket policies with a Principal of an external AWS account.

Real-world use cases

  • Host a public static website on S3 with a bucket policy allowing anonymous read-only access to objects (e.g., a React app).
  • Allow a partner company's IAM role to upload logs to your bucket, using a bucket policy with a Principal of that account's root.
  • Restrict access to your bucket to only a specific corporate IP range, using a bucket policy with a Condition on aws:SourceIp.

Key takeaways

  • Bucket policies are the modern, flexible way to control S3 access; ACLs are legacy and should be disabled where possible.
  • The most restrictive permission wins — a Deny from any policy overrides an Allow.
  • Always use the correct Resource ARN (including /* for object-level actions) in bucket policies.
  • S3 Block Public Access and Object Ownership settings can override or disable your policies, so must be checked.
  • Learn to combine IAM policies and bucket policies — don't rely on one alone for the principle of least privilege.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.