Skip to content
intermediate Phase 16 · Cloud Security Foundations

Preventing Cloud Storage Exposure

Stop public buckets, open blobs, and world-readable snapshots.

50m
0 problems
Topic Progress 0%

How Storage Exposure Happens and Why It's Devastating

How Storage Exposure Happens

Cloud object storage is the most common source of data breaches in cloud environments. The pattern is always the same: a storage resource that should be private is accidentally configured as public, and an attacker discovers it through automated scanning. The breach is not sophisticated — it's a misconfiguration.

The anatomy of a public bucket exposure

Object storage services (S3, Azure Blob, GCS) are designed to support both public and private access. The service works correctly — the customer configures it incorrectly. Here's how it happens:

  1. A developer creates a bucket for application data
  2. The bucket inherits a permissive default or the developer adds a policy to allow public access for testing
  3. The public access is never removed — it gets forgotten in the rush to production
  4. An attacker runs a scanner that checks known bucket names for public access
  5. Data is exfiltrated — often within hours of the bucket being created
# Checking if a bucket is publicly accessible
aws s3api get-bucket-acl --bucket my-app-data
aws s3api get-bucket-policy --bucket my-app-data

# Checking block public access settings
aws s3api get-public-access-block --bucket my-app-data

The four layers of storage access control

Object storage has multiple layers of access control, and a bucket is public if any layer allows it:

  1. Bucket ACLs — legacy access control lists that can grant public-read or public-read-write
  2. Bucket policies — JSON policies that can grant Principal: "*" (anonymous access)
  3. Access Control Lists (ACLs) on individual objects — can override bucket-level settings
  4. Account-level Block Public Access — a guardrail that overrides all of the above

A bucket is only truly private if all four layers are configured correctly. This is why exposure happens — teams fix one layer but miss another.

Real-world impact

Storage exposure breaches have affected major companies:

  • Verizon (2017): A third-party contractor left an S3 bucket public, exposing 14 million customer records
  • Accenture (2017): Four unsecured S3 buckets exposed secret AWS keys and customer data
  • Booz Allen Hamilton (2019): A public S3 bucket exposed classified data from the U.S. National Reconnaissance Office

The common thread: none of these were sophisticated attacks. They were misconfigurations that automated scanners found within hours.

The default-deny principle

The fundamental rule of cloud storage security: object storage should be private by default. Access should be granted through:

  • Signed URLs — time-limited, cryptographically signed URLs that grant temporary access to specific objects
  • Pre-signed POST policies — allow clients to upload directly to storage without exposing the bucket
  • CloudFront signed URLs/cookies — for content delivery with access control
  • IAM-authenticated access — applications access storage through authenticated API calls
# Generating a pre-signed URL for temporary read access (boto3)
import boto3

s3 = boto3.client('s3')
url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'my-app-data', 'Key': 'reports/2026-q3.pdf'},
    ExpiresIn=3600  # URL expires in 1 hour
)
print(url)
# https://my-app-data.s3.amazonaws.com/reports/2026-q3.pdf?X-Amz-Algorithm=...&X-Amz-Expires=3600&X-Amz-Signature=...

The signed URL grants access to one specific object for a limited time. The bucket itself remains private. This is the correct pattern for sharing data from object storage.

Guardrails and Automated Detection

Guardrails and Automated Detection

Relying on developers to correctly configure storage access every time is a losing strategy. The solution is to implement guardrails that prevent exposure before it happens, and automated detection that catches it when guardrails fail.

Account-level Block Public Access

All major cloud providers offer account-level settings that override any bucket-level public access configuration:

AWS S3 Block Public Access:

# Enable all four block public access settings 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"

The four settings:

  • BlockPublicAcls: Rejects any new public ACL on a bucket or object
  • IgnorePublicAcls: Ignores any existing public ACL (treats the bucket as private)
  • BlockPublicPolicy: Rejects any bucket policy that grants public access
  • RestrictPublicBuckets: Restricts access to bucket policies that grant access to the same account only

When all four are enabled, it's impossible to make a bucket public through ACLs or policies. This is the single most effective guardrail against storage exposure.

Azure Blob equivalent:

# Disable public access at the storage account level
az storage account update \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --allow-blob-public-access false

GCS equivalent:

# Enforce uniform bucket-level access
gsutil uniformbucketlevelaccess set on gs://my-bucket

Infrastructure-as-code scanning

The best time to catch a public bucket is before it's created. Scan your Terraform, CloudFormation, or Pulumi code for permissive storage configurations:

# Terraform — DANGEROUS: public-read ACL
resource "aws_s3_bucket" "data" {
  bucket = "my-app-data"
}

resource "aws_s3_bucket_acl" "data" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read"  # This will be flagged by IaC scanning
}

Tools like Checkov, tfsec, and Terrascan detect these patterns at build time:

# Checkov scan for public S3 buckets
checkov -d . --framework terraform --check CKV_AWS_20

# Output: FAILED for resource aws_s3_bucket_acl.data
# CKV_AWS_20: Ensure S3 bucket has block public ACLs enabled

Continuous posture scanning

Even with guardrails, you need continuous scanning to detect:

  • Buckets that were public before guardrails were enabled
  • Snapshots and AMIs that are publicly shared
  • Storage accounts in subscriptions you don't actively manage
# AWS CLI: Find all public S3 buckets
aws s3api list-buckets --query 'Buckets[].Name' --output text | \
  tr '\t' '\n' | \
  while read bucket; do
    public=$(aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null | \
      jq -r '.PublicAccessBlockConfiguration.BlockPublicAcls')
    if [ "$public" != "true" ]; then
      echo "WARNING: $bucket does not have BlockPublicAcls enabled"
    fi
  done

Snapshot and image exposure

It's not just storage buckets. Machine images and snapshots can also be public:

# Check for public AMIs
aws ec2 describe-images \
  --owners self \
  --query 'Images[?Public==`true`].[ImageId,Name]' \
  --output table

# Check for public snapshots
aws ec2 describe-snapshots \
  --owner-ids self \
  --query 'Snapshots[?VolumeSize>=`8`].[SnapshotId,VolumeSize,Description]' \
  --output table

A public snapshot of a production database volume is just as dangerous as a public bucket.

The defense-in-depth approach

Effective storage exposure prevention uses multiple layers:

  1. Account-level block public access — prevents new public buckets
  2. IaC scanning — catches misconfigurations before deployment
  3. Continuous posture scanning — detects existing public resources
  4. Bucket policy conditions — even if public, restrict by IP or VPC endpoint
  5. Monitoring and alerting — notify when bucket policies change
# CloudWatch Events rule to detect public bucket policy changes
import boto3
import json

# Event pattern for PutBucketPolicy
{
  "source": ["aws.s3"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["s3.amazonaws.com"],
    "eventName": ["PutBucketPolicy"]
  }
}

The goal is not to rely on any single control, but to create a system where exposure is prevented by default, caught early when it happens, and alerted immediately.

Practice Problems

0 / 2 solved
Audit a Bucket for Public Access

You suspect an S3 bucket named 'company-uploads' may be publicly accessible. Write the AWS CLI commands to check all four layers of access control (ACL, bucket policy, object ACL, and block public access settings).

Solution
```bash
# 1. Check Block Public Access settings
aws s3api get-public-access-block --bucket company-uploads

# 2. Check bucket ACL
aws s3api get-bucket-acl --bucket company-uploads

# 3. Check bucket policy
aws s3api get-bucket-policy --bucket company-uploads

# 4. Check if any objects have public ACLs
aws s3api list-objects-v2 --bucket company-uploads --query 'Contents[].Key' --output text | \
  tr '\t' '\n' | \
  while read key; do
    acl=$(aws s3api get-object-acl --bucket company-uploads --key "$key" 2>/dev/null)
    echo "$key: $acl"
  done
```
A bucket is only truly private if all four layers show no public access. The most common gap is having Block Public Access disabled while the bucket policy grants `Principal: "*"`.
Design a Secure File Sharing Pattern

An application needs to allow users to upload and download their own files. The files should be private to each user. Design the access pattern without making the bucket public.

Solution
Use pre-signed URLs with a private bucket:

1. Keep the bucket completely private (enable all Block Public Access settings)
2. For uploads: generate a pre-signed POST URL that allows the client to upload directly to S3
3. For downloads: generate a pre-signed GET URL that allows temporary access to the specific object
4. Store object keys with user prefixes: `user-123/documents/report.pdf`

```python
# Upload: pre-signed POST
post = s3.generate_presigned_post(
    Bucket='my-app-data',
    Key=f'user-{user_id}/{filename}',
    ExpiresIn=300
)

# Download: pre-signed GET
url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'my-app-data', 'Key': f'user-{user_id}/{filename}'},
    ExpiresIn=300
)
```
The bucket remains private at all times. Access is granted per-object, per-user, for a limited duration.

Quiz

1. A bucket has Block Public Access enabled with all four settings. A developer adds a bucket policy with `Principal: "*"` and `s3:GetObject`. What happens?

Question 1 options

2. Which of the following is the correct pattern for allowing users to download their own private files from object storage?

Question 2 options

3. Why is continuous scanning necessary even when account-level Block Public Access is enabled?

Question 3 options

Flashcards

Question

What are the four layers of object storage access control?

Answer

1) Bucket ACLs, 2) Bucket policies, 3) Object-level ACLs, 4) Account-level Block Public Access. A bucket is only private if all four layers are configured correctly.

Question

What is a pre-signed URL and why is it better than a public bucket?

Answer

A pre-signed URL is a time-limited, cryptographically signed URL that grants access to a specific object. The bucket remains private — access is granted per-object, per-user, for a limited duration.

Question

What does account-level Block Public Access do?

Answer

It overrides any bucket-level public access configuration. When all four settings are enabled, it's impossible to make a bucket public through ACLs or policies — the guardrail takes precedence.

Question

Why is continuous scanning necessary even with Block Public Access enabled?

Answer

Block Public Access only covers object storage. Snapshots, AMIs, and other resources can still be public. Resources created before the guardrail was enabled may also still be exposed.

Revision Notes

Key Takeaways

  • 1. Object storage should be private by default — access granted through signed URLs, not public policies
  • 2. Account-level Block Public Access overrides all bucket-level public access configurations
  • 3. A bucket is only private if all four access control layers are configured correctly
  • 4. Continuous scanning is necessary because snapshots, AMIs, and pre-guardrail resources can still be exposed

Interview Tips

  • • Use a concrete example: 'We enabled all four Block Public Access settings at the account level, which made it impossible to create a public bucket through any mechanism.'
  • • Explain the pattern: 'We use pre-signed URLs for file sharing — the bucket stays private and each URL grants time-limited access to one object.'
  • • Discuss the gap: 'Block Public Access only covers S3 — we also scan EBS snapshots and AMIs for public access.'

Cheat Sheet

Preventing Cloud Storage Exposure Cheat Sheet

Phase: Cloud Security Foundations | Level: intermediate

The four layers of storage access control:

  1. Bucket ACLs (legacy)
  2. Bucket policies (JSON)
  3. Object-level ACLs
  4. Account-level Block Public Access (overrides all above)

Block Public Access (all four settings):

  • BlockPublicAcls: Reject new public ACLs
  • IgnorePublicAcls: Ignore existing public ACLs
  • BlockPublicPolicy: Reject public bucket policies
  • RestrictPublicBuckets: Only same-account access

Secure file sharing pattern:

  • Keep bucket private
  • Generate pre-signed URLs for upload/download
  • URLs are time-limited and cryptographically signed
  • No public access needed

Detection layers:

  • IaC scanning (Checkov, tfsec) — catch before deploy
  • Continuous posture scanning — detect existing exposure
  • Monitor bucket policy changes — alert on modification
  • Scan snapshots and AMIs, not just buckets

Common exposure patterns:

  • Principal: "*" in bucket policy
  • public-read ACL
  • Public snapshots of database volumes
  • Buckets created before Block Public Access was enabled

Continue Learning

Before You Start

  • No prerequisites listed.