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:
- A developer creates a bucket for application data
- The bucket inherits a permissive default or the developer adds a policy to allow public access for testing
- The public access is never removed — it gets forgotten in the rush to production
- An attacker runs a scanner that checks known bucket names for public access
- 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:
- Bucket ACLs — legacy access control lists that can grant
public-readorpublic-read-write - Bucket policies — JSON policies that can grant
Principal: "*"(anonymous access) - Access Control Lists (ACLs) on individual objects — can override bucket-level settings
- 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:
- Account-level block public access — prevents new public buckets
- IaC scanning — catches misconfigurations before deployment
- Continuous posture scanning — detects existing public resources
- Bucket policy conditions — even if public, restrict by IP or VPC endpoint
- 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
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: "*"`. 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?
2. Which of the following is the correct pattern for allowing users to download their own private files from object storage?
3. Why is continuous scanning necessary even when account-level Block Public Access is enabled?
Flashcards
Question
What are the four layers of object storage access control?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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:
- Bucket ACLs (legacy)
- Bucket policies (JSON)
- Object-level ACLs
- 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 policypublic-readACL- Public snapshots of database volumes
- Buckets created before Block Public Access was enabled
Continue Learning
Before You Start
- No prerequisites listed.