AWS S3 Security: Policies, Encryption, and Access Control
Secure your AWS S3 buckets with bucket policies, encryption at rest and in transit, access logging, and IAM-based access control best practices.
What you'll learn
- ✓Configure S3 bucket policies and ACLs correctly
- ✓Enable server-side encryption with SSE-S3, SSE-KMS, and SSE-C
- ✓Block public access and enforce HTTPS-only connections
- ✓Set up access logging and monitor bucket activity
Prerequisites
- •AWS account with S3 access
- •AWS CLI installed and configured
- •Basic understanding of IAM policies
Amazon S3 is one of the most widely used AWS services, storing everything from application assets to sensitive customer data. Its flexibility is also its risk — a single misconfigured bucket policy can expose terabytes of private data to the internet. This guide covers the layers of S3 security you should implement to protect your data.
Block Public Access First
AWS provides account-level and bucket-level public access blocks. Enable both as your first line of defense. These settings override any bucket policy or ACL that would otherwise grant public access.
# Block public access at the account level
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
# Block public access at the bucket level
aws s3api put-public-access-block \
--bucket my-secure-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
# CloudFormation
Resources:
SecureBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-secure-bucket
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
Setting ObjectOwnership to BucketOwnerEnforced disables ACLs entirely, which is the recommended approach. ACLs are a legacy access control mechanism and bucket policies provide far more granular control.
Writing Secure Bucket Policies
Bucket policies are JSON documents that define who can do what with your bucket and its objects. A well-crafted bucket policy is your primary access control tool.
Enforce HTTPS Only
Deny all requests that do not use TLS encryption in transit.
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"
}
}
}
]
}'
Enforce Minimum TLS Version
Go beyond just requiring HTTPS by mandating TLS 1.2 or later.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceTLS12",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"NumericLessThan": {
"s3:TlsVersion": 1.2
}
}
}
]
}
Restrict Access to Specific VPCs
For internal data buckets, restrict access to requests originating from your VPC.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VPCOnlyAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::internal-data-bucket",
"arn:aws:s3:::internal-data-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1a2b3c4d"
}
}
}
]
}
Server-Side Encryption
S3 offers three server-side encryption options. Since January 2023, all new objects are automatically encrypted with SSE-S3, but you should understand when to use stronger options.
SSE-S3 (Default)
Amazon manages the keys entirely. This is the simplest option and is now enabled by default.
Resources:
DefaultEncryptionBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
SSE-KMS (Recommended for Sensitive Data)
You control the encryption key through AWS KMS, gaining audit trails, key rotation, and granular access control.
# Create a KMS key for S3 encryption
aws kms create-key \
--description "S3 bucket encryption key" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT
# Configure the bucket to use SSE-KMS
aws s3api put-bucket-encryption \
--bucket my-secure-bucket \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789:key/my-key-id"
},
"BucketKeyEnabled": true
}
]
}'
Enabling BucketKeyEnabled reduces KMS API calls by generating a bucket-level key, which can significantly reduce costs at scale.
Enforce Encryption on Upload
Deny any PutObject request that does not include the correct encryption header.
{
"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"
}
}
}
]
}
IAM Access Control Patterns
Cross-Account Access with IAM Roles
When another AWS account needs access to your bucket, use IAM roles rather than bucket policies with account principals.
Resources:
CrossAccountRole:
Type: AWS::IAM::Role
Properties:
RoleName: s3-cross-account-reader
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: 'arn:aws:iam::987654321098:root'
Action: 'sts:AssumeRole'
Condition:
StringEquals:
sts:ExternalId: 'unique-external-id-here'
Policies:
- PolicyName: S3ReadAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::shared-data-bucket'
- 'arn:aws:s3:::shared-data-bucket/*'
S3 Access Points for Shared Datasets
When multiple teams or applications need different levels of access to the same bucket, use S3 Access Points instead of complex bucket policies.
# Create an access point for the analytics team
aws s3control create-access-point \
--account-id 123456789012 \
--name analytics-ap \
--bucket my-data-bucket \
--vpc-configuration VpcId=vpc-1a2b3c4d
# Set the access point policy
aws s3control put-access-point-policy \
--account-id 123456789012 \
--name analytics-ap \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"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-ap",
"arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-ap/object/*"
]
}
]
}'
Access Logging and Monitoring
Enable Server Access Logging
S3 server access logs capture detailed records of every request made to your bucket.
Resources:
LoggingBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-access-logs-bucket
LifecycleConfiguration:
Rules:
- Id: DeleteOldLogs
Status: Enabled
ExpirationInDays: 90
DataBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-data-bucket
LoggingConfiguration:
DestinationBucketName: !Ref LoggingBucket
LogFilePrefix: s3-access-logs/
CloudTrail Data Events
For compliance-critical buckets, enable CloudTrail data events to get API-level audit logs.
aws cloudtrail put-event-selectors \
--trail-name my-trail \
--event-selectors '[
{
"ReadWriteType": "All",
"IncludeManagementEvents": true,
"DataResources": [
{
"Type": "AWS::S3::Object",
"Values": ["arn:aws:s3:::my-secure-bucket/"]
}
]
}
]'
EventBridge Notifications for Security Alerts
Trigger alerts when bucket configurations change or when objects are accessed unexpectedly.
Resources:
S3SecurityRule:
Type: AWS::Events::Rule
Properties:
Description: Alert on S3 bucket policy changes
EventPattern:
source:
- aws.s3
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- s3.amazonaws.com
eventName:
- PutBucketPolicy
- DeleteBucketPolicy
- PutBucketAcl
- PutPublicAccessBlock
Targets:
- Arn: !Ref SecurityAlertTopic
Id: SecurityAlert
Versioning and Object Lock
Enable Versioning for Data Protection
Versioning protects against accidental deletions and overwrites by keeping all versions of an object.
aws s3api put-bucket-versioning \
--bucket my-secure-bucket \
--versioning-configuration Status=Enabled
Object Lock for Compliance
For regulatory requirements like SEC 17a-4 or HIPAA, use S3 Object Lock to enforce write-once-read-many (WORM) storage.
# Create bucket with Object Lock enabled (must be set at creation)
aws s3api create-bucket \
--bucket compliance-bucket \
--object-lock-enabled-for-bucket \
--region us-east-1
# Set a default retention policy
aws s3api put-object-lock-configuration \
--bucket compliance-bucket \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "COMPLIANCE",
"Days": 365
}
}
}'
A Complete Secure Bucket Template
Here is a CloudFormation template that combines all the practices discussed.
AWSTemplateFormatVersion: '2010-09-09'
Description: Fully secured S3 bucket
Resources:
EncryptionKey:
Type: AWS::KMS::Key
Properties:
Description: S3 bucket encryption key
EnableKeyRotation: true
KeyPolicy:
Version: '2012-10-17'
Statement:
- Sid: EnableRootAccess
Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'kms:*'
Resource: '*'
SecureBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !Ref EncryptionKey
BucketKeyEnabled: true
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
LoggingConfiguration:
DestinationBucketName: !Ref LogBucket
LogFilePrefix: access-logs/
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref SecureBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: DenyInsecureTransport
Effect: Deny
Principal: '*'
Action: 's3:*'
Resource:
- !GetAtt SecureBucket.Arn
- !Sub '${SecureBucket.Arn}/*'
Condition:
Bool:
aws:SecureTransport: 'false'
Wrapping Up
S3 security is not a single setting — it is a layered approach. Start with account-level public access blocks, enforce HTTPS and encryption through bucket policies, use SSE-KMS for sensitive data with key rotation enabled, and monitor access with server access logging and CloudTrail. Disable ACLs in favor of IAM policies and bucket policies for clearer, auditable access control. With these layers in place, your S3 buckets move from potential liability to a hardened data store.
Related articles
- DevOps AWS S3 Bucket Policies Explained
How S3 bucket policies, IAM policies, and ACLs interact, how to write least-privilege bucket policies, and patterns for cross-account access without footguns.
- AWS AWS IAM Least Privilege: Write Secure Policies
Master AWS IAM least privilege principles. Learn to write secure IAM policies, use conditions, and avoid common mistakes that lead to over-permissioned roles.
- AWS AWS IAM Policies vs Roles Cheatsheet
A quick-reference guide to the difference between IAM users, roles, policies, and trust relationships, with examples you can paste into your AWS account today.
- AWS AWS Secrets Manager Tutorial: Storing and Rotating Secrets
A practical guide to AWS Secrets Manager: creating secrets, retrieving them from apps, automatic rotation, IAM access control, and choosing it over SSM Parameter Store.