Amazon S3 is the most widely used cloud storage service, holding petabytes of enterprise data. It is also the source of countless data breaches—nearly all preventable with proper configuration. From the Capital One breach (100 million records) to numerous public bucket exposures, S3 misconfiguration remains a top cloud security risk. This comprehensive guide provides an enterprise security checklist that I have developed through securing S3 infrastructure across financial services and healthcare organizations.
The Defense-in-Depth Model
flowchart TB
subgraph Layer1 ["Layer 1: Account Level"]
A1["Block Public Access (Account)"]
A2["SCPs in AWS Organizations"]
end
subgraph Layer2 ["Layer 2: Bucket Level"]
B1["Block Public Access (Bucket)"]
B2["Bucket Policy"]
B3["ACLs Disabled"]
end
subgraph Layer3 ["Layer 3: Object Level"]
C1["SSE-KMS Encryption"]
C2["Object Lock (WORM)"]
C3["Versioning"]
end
subgraph Layer4 ["Layer 4: Access Level"]
D1["IAM Policies"]
D2["VPC Endpoints"]
D3["Access Points"]
end
subgraph Layer5 ["Layer 5: Detection"]
E1["Access Logging"]
E2["CloudTrail"]
E3["Macie"]
end
Layer1 --> Layer2 --> Layer3 --> Layer4 --> Layer5
style A1 fill:#FFCDD2,stroke:#C62828
style C1 fill:#C8E6C9,stroke:#2E7D32
Layer 1: Account-Level Controls
Block Public Access (Account Level)
This is the single most important control. Enable it at the AWS account level to prevent any bucket from being made public, regardless of bucket policy:
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
What each setting does:
- BlockPublicAcls: Rejects PUT requests that include public ACLs
- IgnorePublicAcls: Ignores existing public ACLs
- BlockPublicPolicy: Rejects bucket policies that grant public access
- RestrictPublicBuckets: Restricts access to buckets with public policies to AWS service principals only
Service Control Policies (SCPs)
In AWS Organizations, use SCPs to enforce S3 security across all accounts:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
},
{
"Sid": "DenyNonKMSEncryption",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
Layer 2: Bucket-Level Controls
Disable ACLs Entirely
S3 ACLs are a legacy access control mechanism that causes confusion. For new buckets, disable them entirely:
aws s3api put-bucket-ownership-controls \
--bucket my-bucket \
--ownership-controls "Rules=[{ObjectOwnership=BucketOwnerEnforced}]"
With BucketOwnerEnforced, ACLs are disabled and the bucket owner automatically owns all objects. All access is controlled via IAM policies and bucket policies only.
Layer 3: Object-Level Encryption
SSE-KMS with Customer Managed Keys
For compliance (HIPAA, PCI-DSS), use SSE-KMS with customer-managed keys (CMK). This provides audit trails via CloudTrail and allows you to revoke access by disabling the key.
{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-xxxx"
},
"BucketKeyEnabled": true
}
]
}
Important: Enable BucketKeyEnabled to reduce KMS costs by up to 99%. Without it, every object operation makes a KMS API call.
Layer 4: Network Controls
VPC Endpoints (Gateway)
For private workloads, access S3 through a VPC Gateway Endpoint. Traffic never leaves the AWS network:
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.us-east-1.s3"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = ["s3:GetObject", "s3:PutObject"]
Effect = "Allow"
Resource = "arn:aws:s3:::my-bucket/*"
Principal = "*"
}]
})
}
Layer 5: Detection and Monitoring
S3 Server Access Logging
Enable access logging to a dedicated logging bucket:
aws s3api put-bucket-logging \
--bucket my-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "my-logs-bucket",
"TargetPrefix": "s3-access-logs/my-bucket/"
}
}'
Amazon Macie for PII Detection
Enable Macie to automatically scan buckets for sensitive data (PII, credentials, financial data):
aws macie2 enable-macie
aws macie2 create-classification-job \
--job-type SCHEDULED \
--s3-job-definition '{"bucketDefinitions": [{"accountId": "123456789012", "buckets": ["my-bucket"]}]}'
Compliance Checklist
| Control | HIPAA | PCI-DSS | SOC 2 |
|---|---|---|---|
| Block Public Access | Required | Required | Required |
| SSE-KMS Encryption | Required | Required | Recommended |
| Access Logging | Required | Required | Required |
| Versioning | Recommended | Recommended | Recommended |
| Object Lock (WORM) | Optional | Required (for logs) | Optional |
Key Takeaways
- Enable Block Public Access at the account level first
- Disable ACLs using BucketOwnerEnforced
- Use SSE-KMS with customer-managed keys for compliance
- Enable Bucket Key to reduce KMS costs
- Use VPC Endpoints for private access
- Deploy Macie for automated sensitive data detection
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.