S3 Bucket Security: Block Public Access, Bucket Policies, and Object Ownership
S3 has two separate access control systems: ACLs (legacy, object-level) and bucket policies (IAM-style, resource-based). Block Public Access settings act as an override that supersedes both. Understanding how the four Block Public Access flags interact prevents accidental public exposure and unexplained access denials.
Two access control systems, one Block Public Access override
S3 has two ways to grant access to objects:
- ACLs (Access Control Lists): object-level or bucket-level permissions using a predefined grant model (READ, WRITE, FULL_CONTROL). Legacy — AWS recommends disabling them.
- Bucket policies: IAM-style JSON policies attached to the bucket. The right approach for all new configurations.
Block Public Access settings are a separate layer that sits on top of both. They prevent public access regardless of what ACLs or bucket policies allow.
The four Block Public Access flags
ConceptAWS S3Block Public Access has four independent settings. Two apply to ACLs; two apply to bucket policies. They can be set at the bucket level or at the AWS account level (account-level settings apply to all buckets in the account, even newly created ones).
Prerequisites
- S3 buckets and objects
- IAM policies
- ACL concept
Key Points
- BlockPublicAcls: prevents new public ACLs from being created. Existing public ACLs are unaffected.
- IgnorePublicAcls: makes Route 53 ignore all public ACLs, even existing ones. ACLs still exist but have no effect.
- BlockPublicPolicy: prevents new bucket policies from granting public access.
- RestrictPublicBuckets: restricts access to any bucket with a public policy to only AWS services and authorized principals.
The four flags in practice
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = true # prevent new public ACLs
ignore_public_acls = true # ignore existing public ACLs
block_public_policy = true # prevent bucket policies granting public access
restrict_public_buckets = true # restrict existing public policy buckets
}
For any bucket that should not be publicly accessible, enable all four. For buckets hosting static websites or public assets where public access is intentional, disable them selectively.
Why all four matter:
BlockPublicAclsalone: prevents new public ACLs but doesn't remove existing onesIgnorePublicAclsalone: disables existing public ACLs but someone could add new ones- Together: prevents and negates public ACLs
- Same logic applies to the policy pair
Bucket policies: the correct way to grant access
# Grant read access to a specific IAM role
resource "aws_s3_bucket_policy" "main" {
bucket = aws_s3_bucket.main.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowRoleAccess"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.app.arn
}
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = "${aws_s3_bucket.main.arn}/*"
},
{
Sid = "AllowListBucket"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.app.arn
}
Action = "s3:ListBucket"
Resource = aws_s3_bucket.main.arn # ListBucket applies to bucket, not objects
}
]
})
}
ListBucket applies to the bucket ARN (without /*). GetObject and PutObject apply to objects (arn:*/*). A common mistake is using arn:.../* for ListBucket — it silently grants nothing because list operations target the bucket, not its contents.
Object ownership and ACL disabling
Modern S3 usage should disable ACLs entirely using Object Ownership settings:
resource "aws_s3_bucket_ownership_controls" "main" {
bucket = aws_s3_bucket.main.id
rule {
object_ownership = "BucketOwnerEnforced" # disables ACLs entirely
}
}
BucketOwnerEnforced: ACLs are disabled. All objects in the bucket are owned by the bucket owner regardless of who uploads them. This is the recommended setting for new buckets — it simplifies access management and prevents cross-account upload confusion.
BucketOwnerPreferred: objects uploaded by other accounts are owned by the bucket owner if the requester includes bucket-owner-full-control canned ACL. Legacy support.
ObjectWriter (default): the uploading account owns the object. Can lead to situations where objects in your bucket are owned by another account and you can't delete them.
📝S3 versioning and lifecycle rules
Versioning keeps all versions of every object. Deleting a versioned object adds a delete marker; the object and all versions remain accessible by version ID.
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
Cost impact: versioning stores every version. An object updated 100 times has 100 stored versions. Lifecycle rules manage this:
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
id = "expire-old-versions"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 30 # delete non-current versions after 30 days
newer_noncurrent_versions = 3 # keep last 3 versions regardless of age
}
abort_incomplete_multipart_upload {
days_after_initiation = 7 # clean up incomplete multipart uploads
}
}
}
abort_incomplete_multipart_upload is easy to forget. Failed multipart uploads leave partial data that accumulates storage charges indefinitely without this rule.
Cross-account access: when bucket policy alone is sufficient
For same-account access, an IAM role's permission policy OR a bucket policy is sufficient. For cross-account access, both the IAM policy (in the requesting account) AND the bucket policy (on the bucket) must allow the action:
# Bucket in Account A — grant access to Account B role
resource "aws_s3_bucket_policy" "cross_account" {
bucket = aws_s3_bucket.main.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::ACCOUNT_B_ID:role/DataProcessor"
}
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.main.arn,
"${aws_s3_bucket.main.arn}/*"
]
}]
})
}
Account B's role also needs permission to call S3 in its own policy. Without the bucket policy, Account B's role can't access Account A's bucket even with full S3 permissions in their own IAM policy.
An S3 bucket has RestrictPublicBuckets=true and a bucket policy that allows s3:GetObject for Principal: *. An IAM user in the same account tries to access an object and gets AccessDenied. An IAM role with explicit s3:GetObject in its policy also gets AccessDenied. What is happening?
mediumThe bucket is not intended to be publicly accessible. The bucket policy was added to allow internal access. Block Public Access settings were not reviewed before adding the bucket policy.
AThe IAM user needs explicit s3:GetObject in their identity policy
Incorrect.Same-account access can be granted by bucket policy alone — no IAM identity policy required. But this isn't the primary issue here.BRestrictPublicBuckets interprets Principal: * as a public policy — it restricts the bucket to only AWS services and authorized IAM principals, overriding the bucket policy's effect for the IAM users
Correct!RestrictPublicBuckets doesn't block access entirely — it restricts what kind of access the public policy grants. A bucket policy with Principal: * is a public policy (anyone on the internet could potentially access it). RestrictPublicBuckets overrides this to allow access only from AWS services and authenticated IAM principals within the same account. However, the IAM role should still work because it's an authenticated IAM principal. The deeper issue: if BOTH the IAM role and user get AccessDenied, the bucket policy might have an explicit Deny, or there's an SCP blocking it. Check for explicit Deny statements.CPrincipal: * in a bucket policy always denies access when Block Public Access is enabled
Incorrect.Principal: * grants public access. RestrictPublicBuckets restricts this to authenticated principals — it doesn't convert the Allow to a Deny for all principals.DBlock Public Access settings override all bucket policies regardless of content
Incorrect.Block Public Access overrides public access (Principal: *), not all access. Policies granting access to specific IAM principals are not affected by Block Public Access.
Hint:RestrictPublicBuckets sees Principal: * as a public policy. What does it do to access from authenticated IAM principals under that setting?