AWS S3 Security Deep Dive: Policies, Encryption, and Access Controls
S3 buckets are the most common target in AWS data breaches. Misconfigured bucket policies, disabled encryption, and overpermissive IAM policies have led to some of the largest cloud data exposures in history. This guide covers the layered security controls that protect S3 data in production environments.
Block Public Access
S3 Block Public Access is an account-level and bucket-level setting that overrides any policy or ACL that would grant public access. Enable it everywhere.
Account-Level Enforcement
# Enable Block Public Access for the entire AWS account
aws s3control put-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
# Verify the setting
aws s3control get-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text)
Use an SCP to prevent anyone from disabling Block Public Access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PreventDisablingS3BlockPublicAccess",
"Effect": "Deny",
"Action": [
"s3:PutBucketPublicAccessBlock",
"s3:PutAccountPublicAccessBlock"
],
"Resource": "*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/SecurityAdminRole"
}
}
}
]
}
Bucket Policies vs ACLs
ACLs are a legacy access control mechanism. AWS now recommends disabling ACLs entirely and using bucket policies and IAM policies for all access control.
# Disable ACLs on a bucket (recommended for all new buckets)
aws s3api put-bucket-ownership-controls \
--bucket my-secure-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
With BucketOwnerEnforced, the bucket owner automatically owns all objects and ACLs are disabled. This eliminates an entire category of misconfiguration.
Server-Side Encryption
Enforcing SSE-KMS with Bucket Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedObjectUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyMissingEncryptionHeader",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}
]
}
Choosing between encryption types:
- SSE-S3 — simplest, no additional cost, AWS manages keys entirely
- SSE-KMS — audit key usage via CloudTrail, control access through KMS key policies, supports automatic key rotation
- SSE-C — you manage keys entirely, AWS never stores them. Use only when regulatory requirements mandate customer-held keys
For most workloads, SSE-KMS with a customer-managed key provides the best balance of security, auditability, and operational simplicity.
S3 Access Points
Access Points provide dedicated access policies for specific applications or teams, simplifying multi-tenant bucket management.
import boto3
import json
s3control = boto3.client('s3control')
account_id = boto3.client('sts').get_caller_identity()['Account']
def create_scoped_access_point(bucket_name, app_name, prefix, vpc_id=None):
"""Create an S3 Access Point scoped to a specific prefix and optional VPC"""
config = {
'AccountId': account_id,
'Name': f"{bucket_name}-{app_name}",
'Bucket': bucket_name,
'PublicAccessBlockConfiguration': {
'BlockPublicAcls': True,
'IgnorePublicAcls': True,
'BlockPublicPolicy': True,
'RestrictPublicBuckets': True
}
}
# Restrict to a specific VPC if provided
if vpc_id:
config['VpcConfiguration'] = {'VpcId': vpc_id}
access_point = s3control.create_access_point(**config)
# Scope the access point policy to a specific prefix
policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": f"Allow{app_name}Access",
"Effect": "Allow",
"Principal": {
"AWS": f"arn:aws:iam::{account_id}:role/{app_name}-role"
},
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
"Resource": [
f"arn:aws:s3:{boto3.session.Session().region_name}:{account_id}:accesspoint/{bucket_name}-{app_name}/object/{prefix}/*",
f"arn:aws:s3:{boto3.session.Session().region_name}:{account_id}:accesspoint/{bucket_name}-{app_name}"
],
"Condition": {
"StringLike": {
"s3:prefix": [f"{prefix}/*"]
}
}
}
]
}
s3control.put_access_point_policy(
AccountId=account_id,
Name=f"{bucket_name}-{app_name}",
Policy=json.dumps(policy)
)
return access_point
# Create access points for different applications
create_scoped_access_point('data-lake', 'analytics', 'analytics/', vpc_id='vpc-abc123')
create_scoped_access_point('data-lake', 'ml-training', 'ml-datasets/', vpc_id='vpc-abc123')
Each access point has its own DNS name and policy, letting teams manage their own access without modifying the bucket policy.
VPC Endpoints for Network Isolation
A gateway VPC endpoint for S3 routes traffic through the AWS network, never touching the internet.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAccessFromOutsideVPC",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "vpce-abc123def456"
}
}
}
]
}
This ensures that even if credentials are exfiltrated, they cannot be used to access the bucket from outside the VPC.
Access Logging and Monitoring
Enable CloudTrail data events for S3 to capture every GetObject, PutObject, and DeleteObject call:
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/"]
}]
}]'
For high-volume buckets where CloudTrail data events are cost-prohibitive, use S3 server access logging as a lower-cost alternative — it captures similar information with eventual consistency delivery.
Securing S3 Access with AccessLens
S3 data breaches almost always stem from IAM misconfigurations — overpermissive roles, wildcard resource grants, or missing VPC conditions. The bucket itself may be locked down, but if an IAM policy grants s3:* on *, those controls are bypassed.
AccessLens helps secure your S3 data by providing:
- Bucket access analysis that maps every IAM principal with access to each S3 bucket
- Overpermissive policy detection that finds
s3:*and wildcard resource grants across your accounts - Cross-account access visibility that reveals which external accounts can reach your buckets
- Risk scoring that prioritizes the S3 access misconfigurations most likely to lead to data exposure
Your bucket policies are only one layer. AccessLens gives you visibility into the IAM policies that ultimately determine who can access your data.
Secure your S3 data with AccessLens and close the IAM gaps that bucket policies alone cannot cover.