S3 Security
Comprehensive guide to S3 security, access control, encryption, and compliance
Securing your S3 buckets is critical. This guide covers access control, encryption, monitoring, and security best practices.
Security Model Overview
Defense in Depth
S3 security uses multiple layers: IAM policies, bucket policies, ACLs, encryption, and monitoring. Use them together for comprehensive protection.
Access Control Methods
S3 provides several mechanisms to control access:
| Method | Scope | Use Case |
|---|---|---|
| IAM Policies | User/Role | Control what principals can do |
| Bucket Policies | Bucket | Resource-based access control |
| ACLs | Object/Bucket | Legacy, avoid if possible |
| Block Public Access | Account/Bucket | Prevent accidental exposure |
| Access Points | Bucket | Simplified access management |
Block Public Access
First Line of Defense
Always enable Block Public Access unless you have a specific need for public access.
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'aws s3api put-public-access-block \
--bucket my-bucket \
--public-access-block-configuration '{
"BlockPublicAcls": true,
"IgnorePublicAcls": true,
"BlockPublicPolicy": true,
"RestrictPublicBuckets": true
}'Block Public Access Settings Explained
| Setting | What It Does |
|---|---|
| BlockPublicAcls | Blocks PUT requests with public ACLs |
| IgnorePublicAcls | Ignores existing public ACLs |
| BlockPublicPolicy | Blocks bucket policies granting public access |
| RestrictPublicBuckets | Restricts access to authorized users only |
Bucket Policies
Resource-based policies attached to buckets:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/ReadOnlyRole"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IPRestriction",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"203.0.113.0/24",
"198.51.100.0/24"
]
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::987654321098:root"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VPCEndpointOnly",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1234567890abcdef0"
}
}
}
]
}aws s3api put-bucket-policy \
--bucket my-bucket \
--policy file://policy.jsonIAM Policies for S3
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}Never use s3:* on * in production. Always scope to specific buckets.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectTagging"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/users/${aws:username}/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["users/${aws:username}/*"]
}
}
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"s3:DeleteObject",
"s3:DeleteObjectVersion"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}S3 Access Points
Simplify access management for shared buckets:
aws s3control create-access-point \
--account-id 123456789012 \
--name my-access-point \
--bucket my-bucket \
--vpc-configuration VpcId=vpc-12345678{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/DataScientist"
},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point/object/data/*"
}
]
}Encryption
Server-Side Encryption
AWS manages keys automatically. Simplest option.
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'aws s3 cp file.txt s3://my-bucket/ \
--sse AES256Use AWS KMS for key management with audit trail.
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/my-key"
},
"BucketKeyEnabled": true
}]
}'aws s3 cp file.txt s3://my-bucket/ \
--sse aws:kms \
--sse-kms-key-id alias/my-keyEnable Bucket Keys to reduce KMS API calls and costs.
Customer provides their own keys. Keys are not stored by AWS.
# Generate a key
openssl rand -out sse-c-key.bin 32
# Upload with customer key
aws s3 cp file.txt s3://my-bucket/ \
--sse-c AES256 \
--sse-c-key fileb://sse-c-key.binYou MUST provide the same key for every GET request. Losing the key means losing the data.
Dual-layer server-side encryption for highest security.
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms:dsse",
"KMSMasterKeyID": "alias/my-key"
},
"BucketKeyEnabled": true
}]
}'Client-Side Encryption
Encrypt before uploading:
# Encrypt with GPG
gpg --symmetric --cipher-algo AES256 sensitive-data.txt
# Upload encrypted file
aws s3 cp sensitive-data.txt.gpg s3://my-bucket/Enforce Encryption
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
},
{
"Sid": "DenyNonKMSEncryption",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}Enforce HTTPS
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}Object Lock (WORM)
Prevent object deletion for compliance:
Object Lock must be enabled when creating the bucket. It cannot be added later.
aws s3api create-bucket \
--bucket my-compliance-bucket \
--object-lock-enabled-for-bucket \
--region us-east-1Protects objects but can be overridden with special permissions.
aws s3api put-object-retention \
--bucket my-bucket \
--key important.pdf \
--retention '{
"Mode": "GOVERNANCE",
"RetainUntilDate": "2025-01-01T00:00:00Z"
}'No one can delete or shorten retention—including root.
aws s3api put-object-retention \
--bucket my-bucket \
--key regulatory.pdf \
--retention '{
"Mode": "COMPLIANCE",
"RetainUntilDate": "2030-01-01T00:00:00Z"
}'Compliance mode retention CANNOT be shortened or removed. Use carefully.
Indefinite protection until explicitly removed.
aws s3api put-object-legal-hold \
--bucket my-bucket \
--key evidence.pdf \
--legal-hold Status=ONaws s3api put-object-legal-hold \
--bucket my-bucket \
--key evidence.pdf \
--legal-hold Status=OFFAccess Logging
Enable server access logging for audit trails:
aws s3api put-bucket-logging \
--bucket my-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "my-logs-bucket",
"TargetPrefix": "s3-access-logs/my-bucket/",
"TargetObjectKeyFormat": {
"PartitionedPrefix": {
"PartitionDateSource": "DeliveryTime"
}
}
}
}'Use partitioned prefixes for easier querying with Athena.
AWS Access Analyzer
Find buckets with public or cross-account access:
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/MyAnalyzer \
--filter '{"resourceType": {"eq": ["AWS::S3::Bucket"]}}'Macie for Data Discovery
Enable Macie to discover sensitive data:
aws macie2 enable-macie
# Create classification job
aws macie2 create-classification-job \
--job-type ONE_TIME \
--name "SensitiveDataScan" \
--s3-job-definition '{
"bucketDefinitions": [{
"accountId": "123456789012",
"buckets": ["my-bucket"]
}]
}'Security Best Practices
Enable Block Public Access
Account-wide and bucket-level for defense in depth.
Use Bucket Policies Wisely
Prefer IAM policies; use bucket policies for cross-account and resource-level conditions.
Enable Default Encryption
Use SSE-KMS with bucket keys for audit trails and cost efficiency.
Enforce HTTPS
Add bucket policy condition to deny non-HTTPS requests.
Enable Versioning
Protect against accidental deletions and overwrites.
Enable Logging
Access logs for audit, CloudTrail for API activity.
Use VPC Endpoints
Keep S3 traffic within AWS network.
Regular Audits
Use Access Analyzer, Config Rules, and Security Hub.
Security Checklist
S3 Security Audit Checklist
- Block Public Access enabled (account + bucket)
- Default encryption enabled (SSE-KMS preferred)
- HTTPS enforced via bucket policy
- Versioning enabled for important data
- Access logging enabled
- CloudTrail data events enabled
- MFA Delete enabled for critical buckets
- IAM policies follow least privilege
- No overly permissive bucket policies
- VPC endpoints configured
- Access Analyzer findings reviewed
- Macie scans for sensitive data