What nobody tells you about S3 permissions until something goes public
How a one-liner exposed eight months of accumulated S3 misconfiguration — and what the permission model looks like when you finally see…
What nobody tells you about S3 permissions until something goes public
How a one-liner exposed eight months of accumulated S3 misconfiguration — and what the permission model looks like when you finally see both layers at once
The bucket had been sitting there for eight months. Logs, backups, a few generated reports — nothing sensitive, just operational noise. Then a developer on the team ran a one-liner to “make the reports accessible for the client.” Thirty-six hours later, our security scanner flagged it. The bucket was public. Not just the reports folder. The entire bucket.
I’m not here to blame anyone. I’ve made variations of this mistake myself. What I want to talk about is why S3 permissions are specifically designed to be misunderstood — and what the mental model actually looks like once you’ve been burned enough times to see it clearly.
Photo by Magnus Skaare on Unsplash
The permission system has two independent layers, and most people only think about one
When you “make an S3 object public,” you’re not flipping a single switch. You’re navigating two separate systems that can contradict each other: IAM policies and bucket ACLs on one side, and the Block Public Access settings on the other. AWS added Block Public Access in 2018 precisely because the original system was too easy to misconfigure. Most tutorials don’t explain this. They show you how to set a bucket policy and stop.
Block Public Access is an account-level and bucket-level override. It doesn’t care what your bucket policy says. If it’s enabled, public access is blocked — period. If it’s disabled, your bucket policy takes over, and that’s where things get interesting.
The problem: these settings live in different places in the console, have different CLI commands, and have different defaults depending on whether the bucket was created before or after 2023. So when someone disables Block Public Access to make one folder accessible, they often don’t realize they’ve just handed the bucket policy full authority — including any wildcard Allow statements that were written months ago by someone who assumed Block Public Access would always be there as a backstop.
What “public” actually means at the object level
Here’s the configuration that bit us. A bucket policy from the early days of the project:
import boto3
import json
s3 = boto3.client('s3')
bucket_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-company-bucket/*"
}
]
}
s3.put_bucket_policy(
Bucket='my-company-bucket',
Policy=json.dumps(bucket_policy)
)
That /* at the end. Every object in the bucket. The developer who wrote this eight months ago wasn't careless — they were following a tutorial for a static site deployment. When the project evolved and the bucket became a general-purpose storage location, nobody updated the policy. Nobody thought to, because Block Public Access was enabled and the bucket showed as "not publicly accessible" in the console.
Disabling Block Public Access didn’t add a new permission. It revealed an existing one.
This is the thing that doesn’t click until you’ve traced it back. The dangerous state was always there.
How to actually audit what’s exposed
After this incident, I wrote a small script we now run on every bucket before any permission change. It checks both layers — not just the policy, but whether Block Public Access is in a state where the policy matters:
import boto3
import json
def audit_bucket_exposure(bucket_name: str) -> dict:
s3 = boto3.client('s3')
# Check Block Public Access
bpa = s3.get_public_access_block(Bucket=bucket_name)
bpa_config = bpa['PublicAccessBlockConfiguration']
all_blocked = all([
bpa_config.get('BlockPublicAcls', False),
bpa_config.get('IgnorePublicAcls', False),
bpa_config.get('BlockPublicPolicy', False),
bpa_config.get('RestrictPublicBuckets', False),
])
result = {
'bucket': bucket_name,
'block_public_access_fully_enabled': all_blocked,
'has_public_policy': False,
'public_statements': []
}
if all_blocked:
return result # BPA overrides everything; stop here
# Only check the policy if BPA isn't fully locked down
try:
policy_response = s3.get_bucket_policy(Bucket=bucket_name)
policy = json.loads(policy_response['Policy'])
for stmt in policy.get('Statement', []):
principal = stmt.get('Principal', '')
effect = stmt.get('Effect', '')
is_public_principal = principal == '*' or principal == {'AWS': '*'}
if effect == 'Allow' and is_public_principal:
result['has_public_policy'] = True
result['public_statements'].append(stmt.get('Sid', 'unnamed'))
except s3.exceptions.from_code('NoSuchBucketPolicy'):
pass
return result
The key line is the early return when Block Public Access is fully enabled. Don’t audit the policy in isolation. A policy with Principal: "*" is only dangerous if Block Public Access isn't covering it. Conflating these two layers is exactly how you end up with false confidence in both directions — thinking something is secure when it isn't, or panicking about a policy that's actually inert.
The thing that actually changes after this happens
Running this script on our other buckets after the incident turned up two more with similar latent policies — both inert because Block Public Access was enabled, but one command away from being live. We locked them down and documented the dependency explicitly.
But the operational change mattered more than the fix. We stopped treating S3 permissions as a one-time configuration and started treating them as a drift problem. Permissions change. Policies get added for specific use cases and never removed. Block Public Access gets disabled for a deployment task and someone forgets to re-enable it. The dangerous state isn’t always introduced intentionally — sometimes it accumulates.
We added the audit script to our pre-deployment checklist and to a weekly cron job that posts results to Slack. Not because we don’t trust each other, but because S3’s permission model is stateful in a way that rewards ongoing attention, not one-time review.
The mental model shift is this: stop thinking about S3 permissions as “what access have I granted” and start thinking about “what would happen if Block Public Access were disabled right now.” That question surfaces the latent risks before anything forces the issue.
What I still don’t have a clean answer to is what to do about old buckets in mature systems — the ones that predate your tenure, have policies written by people who’ve left, and hold data whose sensitivity you’re not entirely sure about. A scheduled audit helps. But the honest answer is that you’re often one configuration change away from discovering something you’d rather not have discovered that way.
메타데이터
- post_id
- 7454d04c704d
- slug
- what-nobody-tells-you-about-s3-permissions-until-something-goes-public-7454d04c704d
- url
- https://aws.plainenglish.io/what-nobody-tells-you-about-s3-permissions-until-something-goes-public-7454d04c704d
- canonical_url
- https://aws.plainenglish.io/what-nobody-tells-you-about-s3-permissions-until-something-goes-public-7454d04c704d
- author_url
- https://medium.com/@m.qasim2782
- status
- ok
- fetched_at
- 2026-06-14 11:28:49