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.

Security Engineerawss3terraform

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:

  1. A bucket policy (or ACL) that grants access to everyone.
  2. 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:

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

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

Enable Default Encryption on an S3 BucketSecurity projectLock Down a Public S3 BucketSecurity projectWrite a Least-Privilege AWS KMS Key PolicySecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

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 →