Securing Amazon S3: Beyond "Block Public Access"

A practical walkthrough of bucket policies, IAM, and encryption for least-privilege S3.

Most S3 data-exposure incidents don't come from a single misconfigured toggle — they come from overly broad policies that accumulate over time. Let's build a least-privilege posture step by step.

Start with account-level guardrails

Enable S3 Block Public Access at the account level, not just per bucket. This is a backstop that prevents an accidental public ACL from ever taking effect.

Tip

Account-level Block Public Access overrides bucket-level settings. Turn it on first, then grant narrow exceptions only where genuinely required (and there usually aren't any).

Write bucket policies that deny by default

A good bucket policy is mostly Deny statements. Here's a policy that enforces TLS and denies any unencrypted upload:

bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "DenyUnEncryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" }
      }
    }
  ]
}

Scope IAM to specific prefixes

Grant access to paths, not whole buckets. Inline Condition keys make this precise:

scoped-iam.json
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-bucket/team-a/*",
  "Condition": {
    "StringLike": {
      "s3:prefix": ["team-a/*"]
    }
  }
}

Watch for wildcard drift

A policy that starts as team-a/* often becomes * after a "temporary" debugging session. Review IAM policies in code review and with automated scanners like IAM Access Analyzer.

Keep traffic off the public internet

For internal workloads, use a VPC Gateway Endpoint for S3 so requests never traverse the public internet, and pair it with a bucket policy that only allows access from that endpoint.

Defense in depth

Endpoint policies + bucket policies + IAM = three independent layers. An attacker must defeat all three, not just one.

Verification checklist

Pre-production S3 checklist
  • Account-level Block Public Access: on
  • Default encryption (SSE-KMS): on
  • Bucket policy denies non-TLS and unencrypted PUTs
  • IAM scoped to prefixes, no s3:* on *
  • Access logging + CloudTrail data events enabled
  • IAM Access Analyzer finding review: clean

Least privilege on S3 isn't one setting — it's a set of independent controls that each assume the others might fail.