Cloud Engineering in Production · Security Incidents
The Bucket Anyone Could List
A bucket policy with Principal * exposed customer files. Block public access at the account level, serve public content through a CDN, and share files with presigned URLs.
Scenario
Example team: a marketing site needed product images served from S3. To make it work quickly, an engineer added a bucket policy allowing s3:GetObject and s3:ListBucket to "Principal": "*". Months later, the support team started storing exported customer invoices in a folder of the same bucket.
What you see in production
A security researcher emails: they found 40,000 customer invoices by listing the bucket. Logs show the bucket was listed by many unknown IPs. The company has to notify customers and regulators.
The naive code
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::acme-assets", "arn:aws:s3:::acme-assets/*"]
}]
}
Reproduce it
A quick check of bucket policies for statements that grant anonymous access, the same thing tools like IAM Access Analyzer do (much more thoroughly):
buckets = {
"acme-assets": [{"Effect": "Allow", "Principal": "*", "Action": ["s3:GetObject", "s3:ListBucket"]}],
"acme-backups": [{"Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::111122223333:role/backup"},
"Action": ["s3:PutObject"]}],
"acme-cdn-origin": [{"Effect": "Allow", "Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": ["s3:GetObject"],
"Condition": {"StringEquals": {"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1"}}}],
}
def is_public(stmt):
principal = stmt["Principal"]
anyone = principal == "*" or (isinstance(principal, dict) and principal.get("AWS") == "*")
return stmt["Effect"] == "Allow" and anyone and "Condition" not in stmt
for name, statements in buckets.items():
public = [s for s in statements if is_public(s)]
actions = sorted({a for s in public for a in s["Action"]})
print(f"{name:16} {'PUBLIC: ' + ', '.join(actions) if public else 'private'}")
Output
acme-assets PUBLIC: s3:GetObject, s3:ListBucket acme-backups private acme-cdn-origin private
Root cause
Account- and bucket-level Block Public Access was turned off for a quick fix, a policy granted access to everyone (including listing the bucket), and the bucket's purpose changed without anyone revisiting its permissions. Public assets and private data shared one bucket.
The fix
- Block Public Access at the account level (and organisation level with an SCP preventing it from being turned off). New buckets have Block Public Access on and ACLs disabled by default; keep it that way.
- Serve public content through a CDN: CloudFront with origin access control, so only the distribution can read the bucket.
- Share private files with presigned URLs that expire, generated by the application after an authorisation check.
- Separate buckets by data classification; never mix public assets and customer data.
- Continuously check: IAM Access Analyzer (external access findings), Macie for sensitive data, Config rules.
| AWS | Azure | Google Cloud | |
|---|---|---|---|
| Prevent public access | S3 Block Public Access | Disallow anonymous blob access on the storage account | Public access prevention, uniform bucket-level access |
| Private origin for a CDN | CloudFront origin access control | Front Door with Private Link | Cloud CDN with private origin authentication |
| Temporary access to one file | Presigned URL | SAS token (user delegation SAS) | Signed URL |
Trade-offs
- A CDN adds a component, but also caching, TLS and lower latency for public assets.
- Presigned URLs can be shared further until they expire; keep expiries short for sensitive files.
- Strict guardrails sometimes block legitimate public hosting; handle exceptions in a dedicated, reviewed account.
Detect it in production
- Access Analyzer findings for buckets shared outside the account; Security Hub/Config rules for public buckets.
- S3 server access logs or CloudTrail data events for
ListBucketfrom unknown principals. - Macie alerts for personal data in buckets that shouldn't have it.
Key points
- Keep Block Public Access on at the account and organisation level.
- Public assets go through a CDN with a private origin; private files through presigned URLs.
- Separate data by classification and continuously scan for external access.
Practice questions
How do you prevent S3 buckets from ever becoming public?
Answer
Enable S3 Block Public Access at the account level, protect that setting with an SCP, keep ACLs disabled, and monitor with IAM Access Analyzer and Config rules.
How should a website serve images from a private S3 bucket?
Answer
Through CloudFront with origin access control: the bucket policy allows only that distribution to read objects, and the bucket stays private.
What are the Azure and Google Cloud equivalents of S3 Block Public Access?
Answer
Azure: disallow anonymous (public) blob access on the storage account. Google Cloud: public access prevention on buckets or by organisation policy, plus uniform bucket-level access.
Open in the app to mark lessons complete, take notes and track your progress.