Google ships a control that makes this entire bug class impossible: Public Access
Prevention. Turn it on at the organization level and no one (no rushed engineer, no
copy-pasted Stack Overflow command, no Terraform module someone vendored in 2021) can
ever grant allUsers access to a bucket again. The binding is simply rejected. It is a
hard, org-wide control, and it is the single most effective thing you can do about GCS
exposure.
Most organizations never set it. So instead of one switch, they are betting that every
operator on every project, forever, never runs one bad
add-iam-policy-binding --member=allUsers. That is the real security posture of most GCS
estates, and it is why open GCS buckets stay common: a single binding to the allUsers
principal lets any anonymous HTTP client list and read the bucket, exactly like an open
S3 bucket.
How GCS buckets become publicly accessible
Google Cloud Storage access is governed by Cloud IAM. Every bucket has an IAM policy that specifies who can do what. Two special principal identifiers can make a bucket public:
allUsers: any caller on the internet, authenticated or not. No Google account is required. This is the principal that creates fully anonymous public access.allAuthenticatedUsers: any caller authenticated with any Google Account or service account.
People routinely treat allAuthenticatedUsers as the "safe" version of public, because
it needs a login. Read it again: any Google account. A free personal Gmail
address someone created in thirty seconds is an authenticated Google account. So is
every other Gmail address on earth. This principal does not mean "people in my org" or
"people I've invited"; it means roughly four billion strangers who happen to have a
Google login. As an access boundary it is barely distinguishable from allUsers, and
treating it as a meaningful restriction is one of the more dangerous assumptions in GCS.
The most common role granted to these principals is Storage Object Viewer
(roles/storage.objectViewer), which allows reading and listing all objects in the
bucket. Granting allUsers this role is equivalent to making the bucket fully public.
Other roles that also expose data include Storage Legacy Bucket Reader
(roles/storage.legacyBucketReader), which grants list access.
Uniform Bucket-Level Access vs legacy ACLs
GCS has two access-control systems, and you need to understand both to audit a bucket fully.
Uniform Bucket-Level Access (UBLA) uses only Cloud IAM policies. When UBLA is enabled on a bucket, object-level ACLs are disabled and all access decisions are made by the bucket's IAM policy. This is the simpler, easier-to-audit model.
Legacy access control uses both Cloud IAM policies and per-object ACLs. When UBLA
is disabled (the legacy mode), object-level ACLs can grant public access independently
of the bucket's IAM policy. A bucket IAM policy might look locked down while individual
objects still carry allUsers ACLs from an earlier configuration. Buckets created
before UBLA became generally available in 2019, and buckets created without explicitly
enabling UBLA, may carry this legacy mode with inconsistent object-level ACLs.
Public Access Prevention, in detail
Public Access Prevention (PAP) is the control from the opening. It blocks
allUsers and allAuthenticatedUsers from ever being granted IAM roles on a bucket or
its objects, and it blocks public ACLs too. You can set it on an individual bucket, or
(far better) enforce it across a whole project or organization through the
constraints/storage.publicAccessPrevention Organization Policy constraint. When PAP is
enforced, any IAM binding that would open a bucket to those two principals is rejected
at the API, not merely flagged after the fact.
It is not on by default. A bucket created today is perfectly happy to accept an
allUsers binding unless something above it says no. Set the org policy once and the bad
command stops working everywhere, for everyone, including the parts of your estate you
have forgotten you own.
How to detect anonymous access
Unauthenticated listing. GCS exposes two public listing endpoints. If a bucket
grants allUsers list access, the following requests return an object manifest
without any Authorization header:
JSON API (returns JSON):
GET https://storage.googleapis.com/storage/v1/b/<bucket-name>/o
XML API (returns XML):
GET https://storage.googleapis.com/<bucket-name>
A 200 response with XML object data confirms public list access. The signal that a
bucket is not publicly listable is a 403: AccessDenied from the XML API, or a
forbidden reason from the JSON API. Do not read accessNotConfigured as "the bucket
is private": it means the Cloud Storage JSON API is not enabled in your calling
project, which is a problem with your request, not with the target bucket's exposure.
Google Cloud CLI IAM policy check. If you own the bucket or have sufficient IAM permissions:
gcloud storage buckets get-iam-policy gs://<bucket-name>
Review the output for any binding where the members list contains allUsers or
allAuthenticatedUsers. Any such binding represents a public access grant.
Check UBLA status.
gcloud storage buckets describe gs://<bucket-name> \
--format="value(iamConfiguration.uniformBucketLevelAccess.enabled)"
If the result is False, the bucket uses legacy ACLs and you should also audit
object-level ACLs for allUsers grants.
Check a GCS bucket here. Paste the storage.googleapis.com URL into the
open-bucket viewer to probe anonymously and confirm what an unauthenticated
caller can access.
What's at risk
Public listing via the XML or JSON API returns every object's name, size, content
type, and last-modified timestamp. This is enough to identify what's in the bucket
and begin targeted downloads. Filename patterns alone can reveal sensitive content:
user-export-2025-08-01.csv, backup-db-prod.tar.gz, config-secrets.json.
Public object read allows downloading any object whose URL is known or guessable. When combined with listing, this means full enumeration and download of every file in the bucket with a simple loop.
What makes GCS exposure sting is what GCS is used for. It is the default landing zone
for data pipelines, so BigQuery table exports (frequently full customer or financial
tables, dumped flat as CSV or Avro) end up sitting in a bucket. It is where Cloud Build
drops artifacts, which means compiled binaries and the intermediate build state that
comes with them. Teams pipe Cloud Logging and Audit log exports into it, handing an anonymous
reader a map of the internal infrastructure. And because a bucket is cheap and
convenient, static site assets routinely share one with the application config that was
never meant to leave the building. The same anonymous GET reaches all of it.
Remediation
Remove allUsers and allAuthenticatedUsers IAM bindings. For each offending binding:
gcloud storage buckets remove-iam-policy-binding gs://<bucket-name> \
--member=allUsers \
--role=roles/storage.objectViewer
Repeat for allAuthenticatedUsers and for any other role that grants list or read
access. Run gcloud storage buckets get-iam-policy again afterward to confirm the
binding is gone.
Enable Uniform Bucket-Level Access. Switching a bucket to UBLA disables object-level ACLs and centralizes all access control in the IAM policy, making the bucket's exposure surface easier to audit:
gcloud storage buckets update gs://<bucket-name> \
--uniform-bucket-level-access
Note: enabling UBLA is reversible for 90 days after it is set; after 90 days, it cannot be disabled.
Enable Public Access Prevention. To prevent any future grant of allUsers or
allAuthenticatedUsers on the bucket:
gcloud storage buckets update gs://<bucket-name> \
--public-access-prevention
To enforce PAP across an entire project or organization, set the Organization Policy
constraint constraints/storage.publicAccessPrevention to Enforced at the desired
resource hierarchy level via the Google Cloud console or the gcloud org-policies
command.
Use signed URLs for time-limited sharing. Rather than granting permanent public access for distribution use cases, generate signed URLs that expire after a set duration:
gcloud storage sign-url gs://<bucket-name>/<object-name> \
--duration=1h \
--private-key-file=<service-account-key.json>
Signed URLs grant read access to one object for one time period and cannot be used to enumerate other objects in the bucket.
Rotate credentials found in previously-public buckets. Any secrets, API keys, or credentials that may have been stored in the bucket's objects should be considered compromised for the duration the bucket was public. Rotate all such credentials immediately, regardless of what access logs show.
For a full index of publicly accessible Google Cloud Storage buckets found by this service, see the GCS exposure index. To test a specific GCS bucket URL right now, use the open-bucket viewer.