13.4 Bucket Policies vs ACLs vs IAM Policies: Choosing the Right Tool
Right, let’s talk about the unholy trinity of AWS access control. This is where most people’s eyes glaze over, and I don’t blame them. AWS has, in its infinite wisdom, given us three different ways to say “yes, you can have that file” or “absolutely not, get lost.” They are: Bucket Policies, ACLs, and IAM Policies. They all seem to do the same thing, which is why it’s so confusing when one works and the other doesn’t. Think of it not as redundancy, but as having a scalpel, a saw, and a sledgehammer. You could use the sledgehammer for brain surgery, but you probably shouldn’t.
The golden rule, the one you should tattoo on your forearm, is this: An explicit DENY in any of these three systems overrides an explicit ALLOW in any other. AWS is paranoid by default. If one policy says “no,” it’s no. Conversation over.
The Legacy Scapegoat: Bucket ACLs
Let’s start with the one we all love to hate: Bucket Access Control Lists (ACLs). These are the crusty old legacy way of controlling access. They’re simplistic, clunky, and frankly, a bit of a security nightmare if used incorrectly. An ACL is a list of grants attached directly to a bucket or an object that specifies which AWS accounts or pre-defined groups (like “Everyone”) are granted access.
The biggest problem? They grant permission directly to an external AWS account ID or a public group. This means your bucket’s security is now dependent on another account’s internal security. If that other account gets compromised, so does your data. Yikes.
You’ll still need to use them for one very specific, very annoying reason: managing access to the bucket’s log delivery group for S3 server access logging. AWS itself requires an ACL grant to write logs to your bucket. It’s a bizarre, stubborn holdover.
# The one acceptable use case for an ACL (grudgingly)
aws s3api put-bucket-acl \
--bucket my-bucket-logs \
--acl LogDeliveryWrite
My advice? Disable ACLs entirely (you can set a bucket setting to forbid them) and pretend they don’t exist unless AWS forces your hand.
The Bucket’s Bouncer: Bucket Policies
This is your primary tool. A Bucket Policy is a JSON-based IAM policy that you attach directly to the bucket itself. It’s the bouncer standing at the door of the club (your bucket), checking IDs (IAM principals) against a list.
This is where you define rules like “Allow everyone to GetObject from the public-read folder” or “Deny all requests that don’t come from our corporate VPN’s IP range.” It’s fantastic for these broad, bucket-level rules. The “Principal” in a bucket policy can be an IAM User/Role from your account, an IAM Role from another account (which is huge for cross-account access), or even the terrifying "*" which means “everyone on the internet.”
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/TheirAppRole"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-cross-account-bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.0.2.0/24"
}
}
}
]
}
This policy is a beauty. It lets a specific role from account 123456789012 read and write objects, but only if the request comes from a specific IP range. This is the kind of precise, secure control bucket policies are made for.
The User’s Badge: IAM Policies
While a bucket policy is attached to the resource (the bucket), an IAM policy is attached to the principal (the user or role) in your account. It’s the badge your employee carries that says what buildings they’re allowed into.
The main advantage here is centralization. You manage all the permissions for your IAM users and roles in one place—the IAM console—instead of having to hop into the properties of fifty different buckets. You can create a single IAM policy that grants a user read access to a whole set of buckets that follow a naming convention, like "arn:aws:s3:::app-data-*".
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::app-data-production",
"arn:aws:s3:::app-data-production/*"
]
}
]
}
This policy lets a user list all buckets (you kinda need this for the console to work) but only actually access the app-data-production bucket. The key difference? As an IAM policy, it can only grant permissions to principals within your own account. You cannot use an IAM policy to grant access to a user in another account; for that, you need a bucket policy.
How to Choose: A Practical Flowchart
Stop guessing. Follow this logic:
Is the principal you’re granting access to in the same AWS account as the bucket?
- Yes: Use an IAM Policy. Attach the permission directly to the user/role. It’s cleaner and more portable.
- No: You must use a Bucket Policy. The bucket needs to explicitly allow the foreign principal.
Are you creating a broad, bucket-level rule based on non-identity factors (like IP address, SSL requirement, or making a bucket public)?
- Yes: This is Bucket Policy territory. It’s the right tool for the {{< bibleref “Job 3 ” >}}. Are you setting up S3 Server Access Logging?
- Yes: Sigh. You have to use a Bucket ACL for the log delivery group. It’s the exception that proves the rule.
In 99% of modern use cases, your best practice is: Disable ACLs (use S3 “Object Ownership” controls to enforce this), and use a combination of IAM Policies for principals in your account and Bucket Policies for cross-account access and broad resource-based rules. This gives you the strongest, most manageable, and least confusing security posture. Now you know. Go forth and stop making buckets accidentally public.