BlogCloud

Locking down S3: block public access and enforce encryption

Turn on account-wide Block Public Access, require TLS and encryption with a bucket policy, and verify it.

Jul 2, 2025·4 min readBeginner·By the SecOpsLog team · command-tested

Nearly every headline S3 breach is the same story: a bucket left public, objects uploaded without encryption, or traffic allowed over plain HTTP. AWS gives you layered controls — account-wide Block Public Access, bucket default encryption, and a bucket policy that denies insecure transport — and most incidents happen because someone skipped the first layer and assumed the bucket name was secret enough.

This note turns on account-level public access blocks, sets default SSE-S3 (or SSE-KMS where your org requires it), attaches a deny-insecure-transport policy, and verifies with CLI commands you can paste into a runbook. For how S3 fits into the wider AWS architecture exam lens, see AWS Solutions Architect Associate; for org guardrails and detective controls, see AWS security for DevOps.

Defense in depth for a single bucket

Account BPA is the master switch. Bucket settings and policies add encryption and TLS requirements even when a future change tries to open the bucket.

1Account BPAfour flags true2Bucket BPAinherit + confirm3Default encryptionSSE-S3 or SSE-KMS4Bucket policyDeny aws:SecureTransport=false5Block public ACLsno canned-public-read6Access Analyzerexternal access findings7aws s3api verifyprove controls in CI

Block public access at the account — then the bucket

Account-level Block Public Access overrides public bucket policies and ACLs. Turn all four settings on for the account first, then confirm each sensitive bucket inherits them. This is the control that stops a well-meaning Principal: * policy from actually exposing data.

account-bpa.sh
aws s3control put-public-access-block \
--account-id 111122223333 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-public-access-block \
--bucket acme-app-data \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true

Default encryption so clients cannot forget

Set default bucket encryption so every PutObject is encrypted even when the client omits ServerSideEncryption. SSE-S3 (AES256) is zero-config; SSE-KMS adds per-object key control and CloudTrail data events on KMS usage — pick what your compliance regime requires. There is no separate “deletion protection” toggle on KMS keys comparable to RDS; protect keys with key policies, IAM, and optional DeletionWindowInDays when scheduling key deletion.

bash — enable default SSE-S3live
aws s3api put-bucket-encryption --bucket acme-app-data \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
aws s3api get-bucket-encryption --bucket acme-app-data
SSEAlgorithm: AES256

Deny cleartext and require encryption on write

A Deny on aws:SecureTransport = false blocks HTTP. Add a second statement denying PutObject when s3:x-amz-server-side-encryption is absent if you need to enforce KMS on every upload. Test policies with the IAM policy simulator before you attach — a typo in Resource ARNs fails open in ways that look fine in the console.

Enable S3 server access logging or CloudTrail data events on sensitive prefixes before you declare victory — detective controls tell you when a misconfigured lifecycle rule or cross-account copy bypassed your intent. Block Public Access stops accidental publicity; logging tells you when someone tried anyway.

bucket-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::acme-app-data",
"arn:aws:s3:::acme-app-data/*"
],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
},
{
"Sid": "DenyUnencryptedObjectUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::acme-app-data/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
]
}
Public block is not a substitute for IAM
Block Public Access stops the internet from reading your bucket. It does not stop another AWS principal in your org with s3:GetObject on `*` from exfiltrating data. Pair BPA with least-privilege IAM and S3 access logging or CloudTrail data events on sensitive prefixes.

Verify with commands, not console eyeballing

bash — confirm BPA and encryptionlive
aws s3api get-public-access-block --bucket acme-app-data
BlockPublicAcls: true (all four flags)
aws s3api get-bucket-policy-status --bucket acme-app-data
IsPublic: false
curl -I http://acme-app-data.s3.amazonaws.com/
403 Forbidden — TLS-only policy blocks HTTP
SSE-S3 vs SSE-KMS
SSE-S3 (AES256)
No KMS API calls per object
Simpler, cheaper at scale
Good default for most app data
SSE-KMS
Per-object key via CMK
KMS grants + CloudTrail
Required by some compliance regimes

Where this goes next

Bucket hardening is one resource type. Extend the same discipline to IAM roles that can read the bucket, enable S3 Object Lock for WORM workloads, and wire Checkov or AWS Config rules so a public ACL change fails the pipeline before apply. AWS security for DevOps covers Access Analyzer, SCPs, and detective controls across the account.

Go deeper in a courseAWS security for DevOps engineersS3, IAM, OIDC federation, and org-wide guardrails.View course

Related posts