Skip to main content

S3 Security Deep Dive — Bucket Policies, Encryption, and Access Points

· 8 min read
Goel Academy
DevOps & Cloud Learning Hub

An S3 bucket with default settings is not public — but one misconfigured bucket policy or legacy ACL can expose every object in it to the entire internet. Capital One's 2019 breach leaked 100 million records through an SSRF attack that reached an S3 bucket via an overly permissive IAM role. S3 security is not optional, and "it works" is not the same as "it's secure." Let's lock things down properly.

Block Public Access — The First Line of Defense

Before writing a single bucket policy, enable Block Public Access at the account level. This overrides any policy or ACL that would grant public access:

# Block public access at the ACCOUNT level (affects all buckets)
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Verify account-level settings
aws s3control get-public-access-block \
--account-id 123456789012

# Block public access on a specific bucket (defense in depth)
aws s3api put-public-access-block \
--bucket my-secure-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
SettingWhat It Blocks
BlockPublicAclsRejects PUT requests that include public ACLs
IgnorePublicAclsIgnores all public ACLs on the bucket and objects
BlockPublicPolicyRejects bucket policies that grant public access
RestrictPublicBucketsRestricts access to buckets with public policies to AWS principals only

Set all four to true. There is almost never a reason to leave any of them off.

Bucket Policies — Allow and Deny Examples

Bucket policies are JSON documents attached to a bucket. They control who can do what. Always follow the principle of least privilege.

Allow a Specific IAM Role

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAppRoleAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/app-backend-role"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
]
}
]
}

Deny Unencrypted Uploads

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyNoEncryptionHeader",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}

Deny Non-HTTPS Access

# Apply a policy that denies all non-TLS requests
aws s3api put-bucket-policy \
--bucket my-secure-bucket \
--policy '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
}]
}'

ACLs vs Bucket Policies

ACLs are the legacy access control mechanism. AWS now recommends disabling them entirely:

# Disable ACLs — bucket owner enforced (recommended)
aws s3api put-bucket-ownership-controls \
--bucket my-secure-bucket \
--ownership-controls '{
"Rules": [{"ObjectOwnership": "BucketOwnerEnforced"}]
}'

With BucketOwnerEnforced, ACLs are completely disabled. All access is controlled through bucket policies and IAM policies. This eliminates an entire class of misconfiguration.

Server-Side Encryption — SSE-S3 vs SSE-KMS vs SSE-C

Every object in S3 should be encrypted at rest. AWS provides three options:

FeatureSSE-S3SSE-KMSSSE-C
Key managementAWS manages entirelyAWS KMS (you control the key)You provide the key
Key rotationAutomaticAutomatic (or manual)You manage rotation
Audit trailNoCloudTrail logs every key useNo
CostFree$1/month per key + $0.03/10K requestsFree (you manage keys)
Access controlIAM/bucket policy onlyIAM + KMS key policyMust send key with every request
HeaderAES256aws:kmscustomer-provided
Best forGeneral workloadsCompliance, audit requirementsStrict key ownership

Enable Default Encryption

# Enable SSE-S3 as default (AES-256)
aws s3api put-bucket-encryption \
--bucket my-secure-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'

# Enable SSE-KMS as default with a specific KMS key
aws s3api put-bucket-encryption \
--bucket my-compliance-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/abc-def-123"
},
"BucketKeyEnabled": true
}]
}'

S3 Bucket Keys (BucketKeyEnabled: true) reduce KMS API calls by generating a bucket-level key that encrypts objects locally. This can cut KMS costs by up to 99% for high-throughput buckets.

Client-Side Encryption

For maximum control, encrypt before uploading. AWS provides the S3 Encryption Client, but you can also use any encryption library:

# Encrypt a file locally with a KMS key, then upload
aws kms generate-data-key \
--key-id arn:aws:kms:us-east-1:123456789012:key/abc-def-123 \
--key-spec AES_256 \
--query '{Plaintext:Plaintext,Encrypted:CiphertextBlob}' --output json

# The plaintext key encrypts your data locally
# The encrypted key is stored alongside the object as metadata
# On download, KMS decrypts the key, then you decrypt the data locally

S3 Access Points

Access Points simplify managing access to shared buckets. Instead of one massive bucket policy, each application gets its own access point with its own policy:

# Create an access point for the analytics team
aws s3control create-access-point \
--account-id 123456789012 \
--name analytics-access \
--bucket my-data-lake \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Create an access point for the ML team
aws s3control create-access-point \
--account-id 123456789012 \
--name ml-training-access \
--bucket my-data-lake \
--vpc-configuration VpcId=vpc-abc123

# Set access point policy (analytics can only read from /analytics/ prefix)
aws s3control put-access-point-policy \
--account-id 123456789012 \
--name analytics-access \
--policy '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AnalyticsReadOnly",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/analytics-role"
},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-access",
"arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-access/object/analytics/*"
]
}]
}'

The ML team's access point is restricted to a specific VPC (vpc-configuration), so it can only be reached from within the VPC — no internet access at all.

VPC Endpoints for Private S3 Access

By default, S3 traffic from EC2 goes over the public internet (even though it stays on AWS's network). A VPC Gateway Endpoint keeps traffic entirely within the AWS network:

# Create a gateway endpoint for S3
aws ec2 create-vpc-endpoint \
--vpc-id vpc-abc123 \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-abc123 rtb-def456 \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowSpecificBucket",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-secure-bucket/*"
}]
}'

Combine this with a bucket policy that restricts access to the VPC endpoint:

{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyAccessOutsideVPC",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-abc123456"
}
}
}]
}

Now the bucket is only accessible from within your VPC. No internet path exists to reach it.

Object Lock for Compliance

Object Lock enforces WORM (Write Once Read Many) protection. Once locked, an object cannot be deleted or overwritten — even by the root account:

# Enable Object Lock on a new bucket (must be set at creation)
aws s3api create-bucket \
--bucket my-compliance-bucket \
--object-lock-enabled-for-bucket

# Set a default retention policy (Compliance mode — nobody can delete)
aws s3api put-object-lock-configuration \
--bucket my-compliance-bucket \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "COMPLIANCE",
"Years": 7
}
}
}'

Compliance mode prevents anyone — including the root account — from deleting objects before the retention period expires. Governance mode allows users with special permissions (s3:BypassGovernanceRetention) to delete objects early.

S3 Access Analyzer and CloudTrail

Use IAM Access Analyzer to find buckets with public or cross-account access, and CloudTrail data events to audit who accessed what:

# Check for public or cross-account access findings
aws accessanalyzer list-findings \
--analyzer-arn "arn:aws:access-analyzer:us-east-1:123456789012:analyzer/s3-analyzer" \
--filter '{
"resourceType": {"eq": ["AWS::S3::Bucket"]},
"status": {"eq": ["ACTIVE"]}
}' \
--query 'findings[].{Bucket:resource,Access:principal,Actions:action}' \
--output table

# Enable CloudTrail data events for S3
aws cloudtrail put-event-selectors \
--trail-name management-trail \
--event-selectors '[{
"ReadWriteType": "All",
"IncludeManagementEvents": true,
"DataResources": [{
"Type": "AWS::S3::Object",
"Values": ["arn:aws:s3:::my-secure-bucket/"]
}]
}]'

CloudTrail data events log every GetObject, PutObject, and DeleteObject call. This is essential for compliance audits but generates a lot of log volume — enable it selectively on sensitive buckets, not all buckets.

What's Next?

Your S3 buckets are now locked down with defense-in-depth: public access blocked at the account level, encryption enforced, access scoped through access points, and every operation audited. Next, we'll look at AWS Cost Optimization — because security done right also means not paying for storage classes and data transfer patterns you don't need.


This is Part 16 of our AWS series. Block Public Access at the account level, enforce encryption with a deny policy, and use Access Analyzer to catch what you missed. Three steps that prevent 90% of S3 breaches.