S3 Bucket Policies: How Are They Different from IAM Policies?
Learn the differences between S3 bucket policies and IAM policies, JSON syntax, and cross-account access configuration with practical examples.
Related Exam Domains
- Domain 1: Design Secure Architectures
Key Takeaway
Bucket policies define "who can access this bucket," while IAM policies define "what this user can do." Use bucket policies for cross-account access or public access, use IAM policies for managing internal user permissions.
Exam Tip
Exam Essential: Bucket policies require Principal (who). IAM policies don't have Principal (the attached user is the implicit principal). For cross-account access, both bucket policy AND target IAM policy must Allow for access.
| Feature | Bucket Policy | IAM Policy |
|---|---|---|
| Attached To | S3 bucket (resource) | IAM users/roles/groups |
| Principal Element | Required (who accesses) | None (implicit from attachment) |
| Public Access | Possible ("Principal": "*") | Not possible |
| Cross-Account | Standalone possible | Requires role assumption |
| Scope | Specific bucket only | All resources for the user |
Bucket Policy vs IAM Policy: When to Use What?
When to Use Bucket Policy
Bucket Policy Use Cases:
├── Public website hosting (allow all users read)
├── Cross-account access (grant permissions to other AWS accounts)
├── Allow access only from specific IP/VPC
├── CloudFront OAC integration
└── Service Principal access (CloudTrail, ELB logs, etc.)
When to Use IAM Policy
IAM Policy Use Cases:
├── User/role permission management within same account
├── Manage permissions for multiple S3 buckets in one place
├── EC2 instance profile S3 access
├── Lambda function execution role S3 access
└── Fine-grained per-user permission control
Selection Flowchart
Need to set S3 permissions?
│
├── Same account internal users?
│ └── YES → IAM policy (+ bucket policy if needed)
│
├── Access from different AWS account?
│ └── YES → Bucket policy required (+ target IAM policy)
│
├── Need public access?
│ └── YES → Bucket policy required (+ Block Public Access OFF)
│
└── AWS service needs access? (CloudTrail, ELB, etc.)
└── YES → Bucket policy (use Service Principal)
Bucket Policy JSON Structure
Basic Structure
{
"Version": "2012-10-17",
"Id": "ExampleBucketPolicy",
"Statement": [
{
"Sid": "StatementIdentifier",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket-name/*",
"Condition": {}
}
]
}
Element Descriptions
| Element | Required | Description |
|---|---|---|
| Version | ✅ | Policy language version (always use "2012-10-17") |
| Id | ❌ | Policy identifier (optional) |
| Statement | ✅ | Permission rule array (multiple allowed) |
| Sid | ❌ | Statement identifier (for readability) |
| Effect | ✅ | Allow or Deny |
| Principal | ✅ | Permission target (who) |
| Action | ✅ | Actions to allow/deny (what) |
| Resource | ✅ | Target resource (where) |
| Condition | ❌ | Conditional application (under what conditions) |
Principal Syntax
Principal Types with Examples
// 1. All users (public)
"Principal": "*"
// 2. Specific AWS account
"Principal": {"AWS": "arn:aws:iam::123456789012:root"}
// 3. Specific IAM user
"Principal": {"AWS": "arn:aws:iam::123456789012:user/username"}
// 4. Specific IAM role
"Principal": {"AWS": "arn:aws:iam::123456789012:role/rolename"}
// 5. AWS service
"Principal": {"Service": "cloudfront.amazonaws.com"}
// 6. Multiple Principals
"Principal": {
"AWS": [
"arn:aws:iam::111111111111:root",
"arn:aws:iam::222222222222:root"
]
}
Exam Tip
Exam Point: "Principal": "*" means all users (including anonymous). "Principal": {"AWS": "*"} means all authenticated AWS accounts. They're different!
Action Syntax
Commonly Used S3 Actions
| Action | Description | Applies To |
|---|---|---|
s3:GetObject | Read objects | Object |
s3:PutObject | Upload objects | Object |
s3:DeleteObject | Delete objects | Object |
s3:ListBucket | List objects in bucket | Bucket |
s3:GetBucketLocation | Get bucket region | Bucket |
s3:* | All S3 operations | All |
Action and Resource Mapping
// Object-level actions → Resource needs /*
{
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::bucket-name/*"
}
// Bucket-level actions → Resource is bucket only
{
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::bucket-name"
}
// When both needed → Use Resource array
{
"Action": ["s3:ListBucket", "s3:GetObject"],
"Resource": [
"arn:aws:s3:::bucket-name",
"arn:aws:s3:::bucket-name/*"
]
}
Exam Tip
Exam Trap: s3:ListBucket applies to the bucket, s3:GetObject applies to objects. Wrong Resource ARN = permissions won't work!
Practical Bucket Policy Examples
1. Static Website Public Read
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-website-bucket/*"
}
]
}
2. Cross-Account Access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::shared-bucket",
"arn:aws:s3:::shared-bucket/*"
]
}
]
}
3. Allow Only Specific IP Range
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IPAllow",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::secure-bucket",
"arn:aws:s3:::secure-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ["192.168.0.0/24", "10.0.0.0/8"]
}
}
}
]
}
4. Allow Only VPC Endpoint Access
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VPCEndpointOnly",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::private-bucket",
"arn:aws:s3:::private-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1234567890abcdef0"
}
}
}
]
}
5. CloudFront OAC Integration
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::cdn-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
6. Enforce HTTPS (Block HTTP)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyHTTP",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::secure-bucket",
"arn:aws:s3:::secure-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
Condition Keys
Commonly Used Condition Keys
| Condition Key | Purpose | Example Value |
|---|---|---|
aws:SourceIp | IP address restriction | "192.168.1.0/24" |
aws:SourceVpce | VPC Endpoint restriction | "vpce-xxx" |
aws:SourceVpc | VPC restriction | "vpc-xxx" |
aws:SecureTransport | Enforce HTTPS | "true" / "false" |
s3:x-amz-acl | ACL condition | "bucket-owner-full-control" |
s3:prefix | Prefix restriction | "folder/" |
Condition Operators
| Operator | Description |
|---|---|
StringEquals | Exact string match |
StringNotEquals | String doesn't match |
StringLike | Wildcard match (*, ?) |
IpAddress | Within IP range |
NotIpAddress | Outside IP range |
Bool | Boolean condition |
Cross-Account Access Configuration
Both Sides Need Configuration
Cross-account access requires configuration in both the bucket owner account and the accessing account.
Cross-Account Access Setup:
[Account A: Bucket Owner]
├── S3 Bucket Policy
│ └── Principal: Allow Account B
│ └── Action: Allow needed actions
│
[Account B: Accessor]
├── IAM Policy (attached to user/role)
│ └── Resource: Account A's bucket ARN
│ └── Action: Allow needed actions
Example: Account B Accessing Account A's Bucket
Account A (Bucket Owner) - Bucket Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::222222222222:role/DataAccessRole"},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::account-a-bucket",
"arn:aws:s3:::account-a-bucket/*"
]
}
]
}
Account B (Accessor) - IAM Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::account-a-bucket",
"arn:aws:s3:::account-a-bucket/*"
]
}
]
}
Exam Tip
Exam Essential: Cross-account requires bucket policy Allow + IAM policy Allow = Access granted. Missing either = access denied! Within the same account, bucket policy alone can grant access.
Policy Evaluation Logic
Allow and Deny Priority
Policy Evaluation Order:
1. Is there an explicit Deny? → Always denied (highest priority)
2. Is there an explicit Allow? → Allowed
3. Neither exists? → Implicit deny (default)
Key: Deny ALWAYS takes precedence over Allow!
When Multiple Policies Exist
Same Account:
├── IAM Allow + No bucket policy → Allowed
├── No IAM + Bucket policy Allow → Allowed
├── IAM Allow + Bucket policy Allow → Allowed
└── Deny anywhere → Denied
Cross-Account:
├── Bucket policy Allow + IAM Allow → Allowed
├── Bucket policy Allow + No IAM → Denied
├── No bucket policy + IAM Allow → Denied
└── Both must Allow!
SAA-C03 Exam Focus Points
Common Question Types
| Type | Key Point |
|---|---|
| Principal difference | Only bucket policies have Principal, IAM policies don't |
| Cross-account | Both sides must Allow (bucket policy + IAM policy) |
| Resource ARN | ListBucket = bucket, GetObject = objects (/*) |
| HTTPS enforcement | Use aws:SecureTransport condition |
| VPC restriction | Use aws:SourceVpce or aws:SourceVpc |
| Deny precedence | Explicit Deny overrides all Allow statements |
Common Traps
❌ Specify Principal in IAM policies
→ IAM policies don't have Principal element
❌ Apply s3:GetObject to bucket ARN
→ Must apply to object ARN (bucket/*)
❌ Cross-account only needs bucket policy
→ Target account's IAM policy also needed
❌ Allow overrides Deny
→ Explicit Deny ALWAYS takes precedence
❌ Set public access with IAM policy
→ Needs bucket policy + Block Public Access OFF
Frequently Asked Questions
Q: Should I check bucket policy or IAM policy first?
Check both. Within the same account, either one allowing is sufficient, but cross-account requires both to Allow. Deny anywhere is applied with highest priority.
Q: Is there a bucket policy size limit?
20KB maximum. For complex policies, separate into IAM policies or use S3 Access Points.
Q: Should I use bucket policies or ACLs?
Use bucket policies. ACLs are legacy, and since 2023, new buckets have ACLs disabled by default. Bucket policies are more granular and easier to manage.
Q: How do I troubleshoot "Access Denied" errors?
- Check Block Public Access settings (for public access)
- Verify bucket policy Principal, Action, Resource
- Check IAM policy (both sides for cross-account)
- Look for explicit Deny statements
- Check VPC Endpoint policy (for VPC internal access)
Q: Can I put multiple Statements in one policy?
Yes, Statement is an array so you can include multiple rules in one policy. Use Sid to clearly distinguish each Statement for readability and management.
Summary
S3 bucket policies are essential for resource-based permission management. Key points:
- Bucket Policy: "Who can access this bucket" (Principal required)
- IAM Policy: "Where can this user access" (no Principal)
- Cross-Account: Both sides must Allow
- Deny Precedence: Explicit Deny overrides all Allow statements
- Resource ARN: Distinguish bucket vs object