Skip to content

S3 Security

Why S3 Shows Up in So Many Breach Reports

S3 itself is secure by default - new buckets are private. Every major "S3 data leak" headline is a misconfiguration, not a flaw in the service: a bucket policy that grants public access, an ACL left open, or encryption/logging simply never turned on. This makes S3 one of the highest-yield things to get right, and one of the most common interview topics for any cloud security role.

Bucket Policies vs. IAM Policies vs. ACLs

Three different mechanisms can grant access to the same object - knowing which one to reach for (and which ones to avoid) matters:

Mechanism Attached To Best For
IAM Policy A user/role Controlling what your own principals can do across many buckets
Bucket Policy The bucket itself Cross-account access, public access (rare, deliberate cases), bucket-wide rules like "deny non-TLS requests"
ACL (Access Control List) Individual objects/buckets Legacy - AWS now recommends disabling ACLs entirely on new buckets

Rule of thumb: use IAM policies for your own principals, bucket policies for cross-account/public rules, and disable ACLs (S3 supports "Bucket owner enforced" mode, which does this for you).

Block Public Access

This is the single highest-leverage control against accidental data exposure - it overrides bucket policies and ACLs that would otherwise make content public, at both the account and bucket level.

# Check if Block Public Access is enabled account-wide
aws s3control get-public-access-block --account-id <account-id>

# Enable it account-wide (should be the default in any serious org)
aws s3control put-public-access-block --account-id <account-id> \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Treat any bucket that needs to be genuinely public (e.g. static website hosting) as an explicit, documented exception - not a default.

A Real Misconfiguration, and the Fix

This is close to the actual policy behind several public breach disclosures:

// Vulnerable: grants read access to literally anyone on the internet
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::customer-data-bucket/*"
    }
  ]
}
// Fixed: scoped to a specific role, over TLS only
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/reporting-service" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::customer-data-bucket/*"
    },
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::customer-data-bucket",
        "arn:aws:s3:::customer-data-bucket/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}

That second statement (deny non-TLS requests) is a best-practice addition worth adding to every bucket policy handling sensitive data, not just public ones.

Encryption Options

Option Who Manages the Key Use When
SSE-S3 AWS Default baseline encryption, no compliance requirement for customer key control
SSE-KMS You (customer-managed key in KMS) You need audit trails on key usage, key rotation control, or cross-account key policies
SSE-C You (you supply the key per request) Rare - you want AWS to never store the key at all

For anything regulated (PCI, HIPAA, customer PII), SSE-KMS with a customer-managed key is the standard answer - it gives you a CloudTrail record every time the key is used to encrypt/decrypt, which SSE-S3 does not.

# Enforce SSE-KMS as the bucket default
aws s3api put-bucket-encryption --bucket customer-data-bucket \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "arn:aws:kms:us-east-1:111122223333:key/abcd-1234"
      }
    }]
  }'

Versioning and MFA Delete

Versioning protects against accidental overwrites/deletes and is a prerequisite for MFA Delete - an extra layer that requires MFA to permanently delete a version or change versioning state, protecting against a compromised credential wiping out data.

aws s3api put-bucket-versioning --bucket customer-data-bucket \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::111122223333:mfa/root-account-mfa-device 123456"

Access Logging

Enable server access logging (or S3 data events in CloudTrail for finer-grained, queryable logs) so you can answer "who accessed this object" during an investigation - without it, you have no forensic trail after the fact.

Best Practices Checklist

  • [ ] Block Public Access enabled at the account level, bucket-level exceptions documented
  • [ ] ACLs disabled (Bucket owner enforced mode)
  • [ ] Bucket policies scoped to specific principals, not "Principal": "*" unless deliberately public
  • [ ] Deny non-TLS requests (aws:SecureTransport: false) on sensitive buckets
  • [ ] Default encryption enabled - SSE-KMS with customer-managed keys for regulated data
  • [ ] Versioning enabled on buckets holding anything you can't afford to lose
  • [ ] MFA Delete enabled on critical buckets
  • [ ] Access logging or CloudTrail data events enabled for audit trail
  • [ ] Regular automated scans (AWS Config rules, Trivy, or third-party) for public buckets

Practice Next

Credits/References

  1. Amazon S3 Security Best Practices
  2. S3 Block Public Access
  3. S3 Server-Side Encryption Options