How to Secure a Leaking S3 Bucket With Terraform
A world-readable S3 bucket almost always comes from a Terraform aws_s3_bucket_policy with Principal = "*" and no Block Public Access. The fix is to delete that policy, add an aws_s3_bucket_public_access_block with all four flags set to true, then layer AWS S3 encryption on top. Here's how.
Why the bucket is public
This bucket is world-readable because the Terraform that provisioned it attaches an
open bucket policy - Principal = "*" with Action = "s3:GetObject" - and nothing
overrides it. Any anonymous request on the internet can GetObject and download the
data. Two conditions have to be true for that to happen, and both are common leftovers:
- A bucket policy (or ACL) that grants access to everyone.
- Block Public Access turned off, so AWS does not veto that policy.
The offending resource looks like this:
resource "aws_s3_bucket_policy" "public_read" {
bucket = aws_s3_bucket.customer_data.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.customer_data.arn}/*"
}]
})
}
Step 1 - delete the public policy
Remove the entire aws_s3_bucket_policy.public_read resource from main.tf. There is
no legitimate reason for a customer-data bucket to grant s3:GetObject to Principal = "*".
If specific objects must be shared, use presigned URLs or CloudFront - never a wildcard grant.
Step 2 - add Block Public Access
Add an aws_s3_bucket_public_access_block with all four flags set to true. This is the
belt-and-braces control: even if someone later re-runs a put-bucket-policy with
Principal = "*", AWS refuses it.
resource "aws_s3_bucket_public_access_block" "customer_data" {
bucket = aws_s3_bucket.customer_data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
What each flag does:
- block_public_acls / ignore_public_acls - reject and ignore any public ACL.
- block_public_policy - reject any new bucket policy that grants public access.
- restrict_public_buckets - restrict access even if a public policy somehow exists.
Step 3 - apply and verify
cd /workspace
terraform apply -auto-approve
aws s3api get-bucket-policy --bucket customer-data # should now fail: no policy
aws s3api get-public-access-block --bucket customer-data # all four true
A successful get-public-access-block returns a PublicAccessBlockConfiguration with every
flag true, and get-bucket-policy no longer returns a wildcard Principal.
Add AWS S3 encryption at rest
Locking down access is half of hardening a bucket - the other half is AWS S3 encryption, so objects are unreadable even if storage is somehow accessed directly. Add a server-side encryption resource next to the bucket:
resource "aws_s3_bucket_server_side_encryption_configuration" "customer_data" {
bucket = aws_s3_bucket.customer_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256" # SSE-S3; use "aws:kms" with a key for SSE-KMS
}
}
}
With default encryption on, every new upload is encrypted at rest even when the client
omits the x-amz-server-side-encryption header. For stricter control, switch
sse_algorithm to aws:kms and reference a customer-managed KMS key so you own the
key rotation and audit trail.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Deleting a wildcard
Principal = "*"bucket policy in Terraform - Adding an
aws_s3_bucket_public_access_blockwith all four flags true - Enabling AWS S3 default encryption at rest with SSE-S3 or SSE-KMS
FAQ
How do I enable AWS S3 encryption in Terraform?
Add an aws_s3_bucket_server_side_encryption_configuration resource pointing at your bucket, with a rule block whose apply_server_side_encryption_by_default sets sse_algorithm to AES256 for SSE-S3 or aws:kms for SSE-KMS. After apply, every new object is encrypted at rest even if the upload omits the encryption header.
How do I secure a public S3 bucket in Terraform?
Delete any aws_s3_bucket_policy that grants Principal = "*", then add an aws_s3_bucket_public_access_block with block_public_acls, block_public_policy, ignore_public_acls, and restrict_public_buckets all set to true. That removes the open grant and blocks any future policy from re-exposing the bucket.
What is the difference between SSE-S3 and SSE-KMS for AWS S3 encryption?
SSE-S3 (AES256) uses keys that AWS fully manages, with no extra setup or cost. SSE-KMS (aws:kms) uses an AWS KMS key you control, giving you key rotation policies, per-key access control, and CloudTrail audit logs for every decrypt - at the cost of KMS request charges.
Does Block Public Access encrypt my S3 data?
No. Block Public Access controls who can reach the bucket, not whether the stored bytes are encrypted. To encrypt data at rest you also need server-side encryption via an aws_s3_bucket_server_side_encryption_configuration resource. Use both together to secure a bucket.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →