You're a security auditor reviewing ten production S3 bucket policies. For each one, decide whether the policy leaks data publicly or is safely scoped. The bucket name in each case is realistic; the company is small enough that the leak would make the news.
Click your answer to reveal the explanation. Pay attention to the principal, the action, the condition, and what's missing as much as what's present. A real S3 audit is mostly reading what isn't there.
Bucket acme-customer-reports
Marketing requested public readThe story: Marketing wants to host downloadable PDF reports on the company website. They asked the SRE team to "make the bucket public." Here's what got applied.
Principal "*" means anonymous internet user. Action "s3:*" includes PutObject, DeleteObject, PutBucketPolicy. Anyone on the internet can read, overwrite, or delete every object — and rewrite the policy itself. The principal should have been scoped (or the bucket fronted by CloudFront with origin access identity); action should have been "s3:GetObject" only.
Bucket acme-static-website
Public read for website hostingThe story: Static website hosting for a marketing landing page. The bucket holds only public HTML/CSS/JS.
Public principal but scoped to s3:GetObject only and to objects under the bucket (not the bucket itself, so ListBucket isn't granted). This is the canonical "public website" policy. It's only safe because the bucket is supposed to be public, the content has been classified as public, and account-level Block Public Access has been configured to allow this specific bucket. In modern AWS, use CloudFront + origin access control instead — you keep the bucket private and serve through a managed CDN.
Bucket acme-prod-backups
Cross-account access requestedThe story: The backup team in a sibling account (111122223333) needs read access to verify backups nightly. An engineer wrote this policy.
"AWS": "...:root" means any IAM principal in account 111122223333, not the root user. So every user, role, and service account in that whole account just got s3:* on the backups bucket — including DeleteObject and PutBucketPolicy. The fix: scope the principal to a specific role (arn:aws:iam::111122223333:role/BackupVerifier) and the action to s3:GetObject + s3:ListBucket.
Bucket acme-prod-backups (v2)
Cross-account, correctedThe story: After the audit finding above, the engineer rewrote the policy.
Principal scoped to a specific role; actions scoped to read-only. The role can list the bucket (needed to enumerate backup files) and get individual objects (needed to verify them). Nothing else. This is the textbook cross-account read-only pattern.
Bucket acme-dev-uploads
Dev environment, but…The story: A development bucket for engineers to upload test data. The team set up a policy "so anyone in our company can use it."
Principal is "*" — not "anyone in our company," but anyone on the internet. The actions include PutObject, so an attacker can upload malware, host phishing pages, or fill the bucket with arbitrary data for which the company gets billed. The intent ("anyone in our company") should have been expressed via IAM identity policies on the company's user roles, not by a public bucket policy.
Bucket acme-app-config
VPC-scoped readThe story: Application servers in a specific VPC read configuration from this bucket. Nothing else should reach it.
Principal scoped to a specific role. Action read-only. The Condition requires the request originate from a specific VPC endpoint — so even a stolen role credential cannot be used from outside that VPC. This is the "defense in depth" pattern that the Capital One breach would have benefited from.
Bucket acme-pii-archive
"Just for the security team"The story: An archive of historical customer PII. Access "limited to the security team."
The principal is one shared IAM user (named "security-team") — meaning multiple humans share the same access key. There is no attribution if something goes wrong. Use a group of named users, or a role assumed via SSO, not a shared user. Also, s3:* gives that shared user delete permission on a PII archive — least privilege says read-only by default, with a separate elevated path for the rare admin task.
Bucket acme-public-assets
"Just images and CSS"The story: Public asset bucket for the company website. Originally locked down with the canonical pattern, but someone added a "convenience" statement.
The first statement is fine. The added s3:ListBucket for "*" lets anyone enumerate every file in the bucket. Even if the files are "just images," an attacker can now discover internal-org-chart-draft.png, q4-board-deck-leaked.pdf, or any file someone "temporarily" uploaded. Never grant public ListBucket.
Bucket acme-prod-logs
CloudTrail log destinationThe story: The CloudTrail service writes audit logs here from every account in the org.
Principal is the CloudTrail service principal (a system-owned identity, not any human or external account). Action is write-only. Resource is scoped to the AWSLogs/ prefix where CloudTrail actually writes. The condition forces the uploaded objects to be owned by the bucket owner (so other accounts can't smuggle objects in with their own ownership). This is the canonical CloudTrail log destination policy.
Bucket acme-prod-data
Trusted partner accessThe story: A trusted partner needs to read specific data. The implementer thought a condition would scope it.
The aws:Referer header is set by the client. An attacker can trivially forge it (curl -H "Referer: https://partner.example.com/anything"). Referer-based access control was a 1998 idea; it has never been a real security boundary. Use IAM principal-based scoping (give the partner a role to assume in your account) instead. This pattern reappears in audit findings every year.
Every leaking policy fails one of four checks. Is the principal too broad (especially "*" when it shouldn't be)? Is the action too broad (especially s3:*)? Is the resource too broad? Is the condition fake security (Referer, IP-allowlist-from-a-shared-IP)? If you read every policy with those four questions, you catch the majority of real-world S3 audit findings.
The fifth check, which doesn't appear in any of these policies but should be in every account: account-level S3 Block Public Access is on. With BPA enabled, even a policy like case 1 fails — the public clauses are silently blocked. BPA does not replace good policy hygiene, but it catches the worst mistakes before they hit production.
References
Formatted in APA 7. Alphabetized by first author's last name.
- Amazon Web Services. (n.d.-a). Bucket policy examples. Amazon S3 User Guide. https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html
- Amazon Web Services. (n.d.-b). Configuring CloudTrail to log to S3. AWS CloudTrail User Guide. https://docs.aws.amazon.com/awscloudtrail/latest/userguide/create-s3-bucket-policy-for-cloudtrail.html
- Amazon Web Services. (n.d.-c). Restricting access to Amazon S3 content by using an origin access identity. Amazon CloudFront Developer Guide. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html