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
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-aclwill fail withAccessDenied. 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
Denystatement that matches the request, it will override anAllowfrom elsewhere. Review all policies attached to the bucket and the identity. - Check the resource ARN: A common mistake is using
arn:aws:s3:::my-bucketwithout the/*for object actions likes3:GetObject. The bucket ARN only covers bucket-level actions (likes3: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-policycall 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 aDenystatement 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:::bucketwithout/*in the Resource for object-level actions (likes3: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
- Use Infrastructure-as-Code tools like Terraform (
aws_s3_bucket_policyresource) or CloudFormation to version-control your bucket policies. - 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.
- 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.