Is my S3 bucket public? Check it on your laptop
"Could anyone on the internet read this object?" is a question worth
answering before the object exists in a real account, not after. With
IAM_ENFORCEMENT=1,
LocalEmu answers it the same way AWS does: an unsigned request is the
anonymous caller, and it gets in only if a bucket policy explicitly
grants public access.
Below, the same object is read three ways: unsigned while private, through a signed presigned link, and unsigned after a deliberate public grant. No AWS account, no cost, and nothing is ever actually exposed to the internet.
Prerequisite
Start LocalEmu with enforcement on so policies are actually applied:
IAM_ENFORCEMENT=1 localemu start.
Without it, every request is permitted and there is nothing to check.
1. A bucket with a private and a public object
Public bucket policies are blocked by default, just like AWS, so the setup turns that block off before anything can be made public.
# awsemu talks to LocalEmu on localhost:4566. No AWS account, no cost.
BUCKET=my-app-assets
awsemu s3api create-bucket --bucket $BUCKET
# Public bucket policies are blocked by default, exactly like AWS. Allow them.
awsemu s3api put-public-access-block --bucket $BUCKET --public-access-block-configuration BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
# One object we want kept private, one we will deliberately expose.
echo 'top secret' > secret.txt
echo 'hello world' > hello.txt
awsemu s3api put-object --bucket $BUCKET --key private/secret.txt --body secret.txt
awsemu s3api put-object --bucket $BUCKET --key public/hello.txt --body hello.txt 2. Private means private
An unsigned read of the private object is denied. There is no bucket policy granting public access, so the anonymous caller has nothing to rely on.
# An unsigned request: a browser, curl, anyone on the internet. No credentials.
curl -s -o /dev/null -w '%{http_code}' http://localhost:4566/$BUCKET/private/secret.txt
# -> 403 denied: the bucket has no policy granting public access 3. A signed link still works
A presigned URL is not anonymous: it carries a real credential in the query string. LocalEmu evaluates it as that principal, so the same private object is readable through the link.
# A presigned link is SIGNED: it carries a real, time-limited credential.
URL=$(awsemu s3 presign s3://$BUCKET/private/secret.txt --expires-in 300)
curl -s -o /dev/null -w '%{http_code}' "$URL"
# -> 200 allowed: evaluated as the real principal, not as anonymous 4. Make one prefix public, on purpose
Save this policy as public-read.json.
It grants anonymous read on public/*
only, deliberately leaving private/ alone.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForPublicPrefix",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-app-assets/public/*"
}
]
} Apply it, then read both objects unsigned. The public one is reachable; the private one is still denied because the grant does not cover it.
awsemu s3api put-bucket-policy --bucket $BUCKET --policy file://public-read.json
# The unsigned request to the public prefix now succeeds...
curl -s -o /dev/null -w '%{http_code}' http://localhost:4566/$BUCKET/public/hello.txt
# -> 200 allowed by the Principal "*" grant on public/*
# ...but the private object is still denied. The grant only covers public/*.
curl -s -o /dev/null -w '%{http_code}' http://localhost:4566/$BUCKET/private/secret.txt
# -> 403 How LocalEmu decides
A request with no credentials is the anonymous principal, evaluated exactly like AWS:
- • There are no identity policies to consider, so access is
granted only when a resource policy names
Principal: "*"for that action. No public grant means denied. - • A signed request (a normal SDK call or a presigned URL) is evaluated as its real principal instead, so it is never treated as anonymous.
- • An explicit
DenyonPrincipal: "*"overrides any allow, the same precedence AWS uses.
This is for catching a public-access mistake inside your iteration loop, at zero cost and with no account. It is not a substitute for AWS or for tools like Access Analyzer; treat a clean local check as fast feedback, not a final audit.
Where to go next
IAM least-privilege
Tighten and test identity policies until they deny exactly what they should.
S3 public-access guardrail
Auto-remediate a bucket that goes public, with CloudTrail and Lambda.
Lambda x3 + least-privilege
A scoped execution role that genuinely denies, across three runtimes.
LocalEmu vs real AWS
What behaves identically, and what does not.