Skip to content
Back to skills

Aws S3 Security

ASecurity

S3 security reference: access control layers, encryption models, a secure bucket baseline in Terraform, TLS-only bucket policies, access points, and anti-patterns such as public buckets and ACL reliance. Use when configuring or auditing S3 buckets.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 1, 2026
securityrustgoawsterraformdatabasesecurity

Works with

  • cli

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add HermeticOrmus/LibreSecOps-Claude-Code --skill aws-s3-security --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Aws S3 Security?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Aws S3 Security
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hermeticormus-aws-s3-security/badge)](https://www.skillsdirectory.com/skills/hermeticormus-aws-s3-security)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: "aws-s3-security"
description: "S3 security reference: access control layers, encryption models, a secure bucket baseline in Terraform, TLS-only bucket policies, access points, and anti-patterns such as public buckets and ACL reliance. Use when configuring or auditing S3 buckets."
---

# AWS S3 Security

> S3 bucket security configurations, access control models, encryption patterns, and data exposure prevention.

## Knowledge Base

### S3 Access Control Layers

S3 has multiple overlapping access control mechanisms. Understanding the hierarchy prevents misconfigurations:

1. **Block Public Access (Account level)** -- Master switch. If enabled at account level, overrides all bucket-level settings. This is your first line of defense.
2. **Block Public Access (Bucket level)** -- Per-bucket controls. Four independent settings:
   - `BlockPublicAcls` -- Rejects PUT requests with public ACLs
   - `IgnorePublicAcls` -- Ignores existing public ACLs
   - `BlockPublicPolicy` -- Rejects bucket policies that grant public access
   - `RestrictPublicBuckets` -- Restricts access to bucket with public policies to AWS principals only
3. **Bucket Policy** -- JSON policies attached to the bucket. Most flexible control.
4. **ACLs (Legacy)** -- Predates bucket policies. AWS recommends disabling ACLs entirely (S3 Object Ownership set to "Bucket owner enforced").
5. **IAM Policies** -- Control what IAM principals can do with S3.
6. **Access Points** -- Named network endpoints with their own access policies, scoped to specific prefixes.
7. **VPC Endpoint Policies** -- Control which buckets can be accessed from a VPC endpoint.

### S3 Encryption Models

| Type | Key | Description | Use Case |
|------|-----|-------------|----------|
| SSE-S3 | AWS managed, auto-rotated | S3 manages everything | Default encryption, no key management overhead |
| SSE-KMS | Customer managed or AWS managed KMS key | Separate key management, audit trail via CloudTrail | When you need key rotation control, access auditing, or cross-account key sharing |
| SSE-C | Customer provided per request | Customer sends key with each request | Regulatory requirement to hold keys outside AWS |
| Client-side | Customer managed entirely | Encrypt before upload | Zero-trust of AWS, end-to-end encryption |

**Default recommendation**: SSE-KMS with a customer-managed key. It provides the best balance of security, auditability, and operational simplicity.

## Patterns

### Pattern 1: Secure Bucket Baseline (Terraform)

```hcl
resource "aws_s3_bucket" "secure" {
  bucket = "my-secure-bucket"
}

resource "aws_s3_bucket_public_access_block" "secure" {
  bucket = aws_s3_bucket.secure.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_ownership_controls" "secure" {
  bucket = aws_s3_bucket.secure.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_versioning" "secure" {
  bucket = aws_s3_bucket.secure.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "secure" {
  bucket = aws_s3_bucket.secure.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_logging" "secure" {
  bucket = aws_s3_bucket.secure.id

  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "s3-access-logs/my-secure-bucket/"
}

resource "aws_s3_bucket_lifecycle_configuration" "secure" {
  bucket = aws_s3_bucket.secure.id

  rule {
    id     = "expire-noncurrent-versions"
    status = "Enabled"

    noncurrent_version_expiration {
      noncurrent_days = 90
    }
  }
}
```

**Why this works**: Every security control is explicit. Public access is blocked at bucket level, ACLs are disabled, versioning protects against deletion, KMS encryption provides auditability, access logging captures all requests, and lifecycle rules manage version accumulation.

### Pattern 2: Bucket Policy Requiring TLS and Limiting Access

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonTLS",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Sid": "RestrictToOrganization",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalOrgID": "o-abc123def4"
        }
      }
    },
    {
      "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"
        }
      }
    }
  ]
}
```

**Why this works**: Three deny statements that cannot be overridden: all traffic must use TLS, all access must come from within the organization, and all uploads must use KMS encryption.

### Pattern 3: S3 Access Point for Scoped Access

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAnalyticsTeamReadOnly",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/analytics-team"
      },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-readonly",
        "arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-readonly/object/data/processed/*"
      ]
    }
  ]
}
```

**Why this works**: Access Points let you create named entry points with their own policies, restricted to specific prefixes. Instead of one complex bucket policy serving many use cases, each consumer gets their own scoped access point.

## Anti-Patterns

### Anti-Pattern 1: Public Bucket for "Convenience"

Making a bucket public because "the app needs to read from it" is never the right answer. Alternatives:
- **CloudFront Origin Access Control (OAC)** -- Serve public content via CloudFront while keeping the bucket private
- **Presigned URLs** -- Grant time-limited access to specific objects
- **S3 Access Points** -- Scoped access for specific applications

### Anti-Pattern 2: Relying on ACLs

ACLs are a legacy access control mechanism from before bucket policies existed. They are confusing, limited, and error-prone. The canonical ACL `public-read` has caused countless data breaches.

**Fix**: Set Object Ownership to "Bucket owner enforced" which disables ACLs entirely. Use bucket policies for all access control.

### Anti-Pattern 3: No Versioning on Critical Data

Without versioning, a compromised credential or ransomware actor can permanently delete or overwrite data. With versioning + MFA Delete, even a compromised root account cannot immediately destroy data.

### Anti-Pattern 4: Same-Account Logging

Storing S3 access logs in the same account as the data means a compromised account can delete its own audit trail. Send logs to a dedicated security/logging account with its own access controls.

### Anti-Pattern 5: Bucket Names Containing Sensitive Information

S3 bucket names are globally unique and can be enumerated. Names like `company-production-database-backups` or `company-pii-data` tell attackers exactly what to target. Use opaque names or at least avoid revealing the data classification.

## References

- [S3 Security Best Practices](https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html)
- [S3 Block Public Access](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html)
- [S3 Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points.html)
- [S3 Encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html)
- [Flaws.cloud](http://flaws.cloud/) -- Educational S3 misconfiguration challenges
- [GrayhatWarfare](https://buckets.grayhatwarfare.com/) -- Public bucket search engine (demonstrates the exposure risk)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…