Back to blog
Best PracticesCloud SecurityGCPIdentity & AccessStorage

Storage Bucket Is Publicly Accessible (GCP Cloud Storage)

Learn why public GCP Cloud Storage buckets leak data, how to remove allUsers and allAuthenticatedUsers bindings, and how to enforce public access prevention.

TL;DR

This check flags Cloud Storage buckets that grant access to allUsers or allAuthenticatedUsers, meaning anyone on the internet can read or list your objects. Fix it by removing those IAM bindings and turning on public access prevention with gcloud storage buckets update gs://BUCKET --public-access-prevention.

Public Cloud Storage buckets are one of the oldest and most reliable ways to leak data, and they keep showing up in breach reports because the failure mode is so easy to hit. A single IAM binding to allUsers on a bucket holding backups, logs, or customer uploads turns a private store into a publicly indexed download endpoint. This Lensix check catches exactly that situation before someone else finds it first.


What this check detects

The storage_public check inspects the IAM policy on every GCP Cloud Storage bucket in your projects. It raises a finding when the bucket policy contains a binding to one of two special principals:

  • allUsers — anyone on the internet, no authentication required. This is fully anonymous access.
  • allAuthenticatedUsers — anyone with a Google account. This is not "your users," it is every Google account on the planet, including throwaway accounts.

If either principal appears with a role like roles/storage.objectViewer, roles/storage.legacyObjectReader, or anything broader, the bucket is treated as publicly accessible. The role determines what an outsider can do, but any of these bindings means data is leaving your control boundary.

Note: GCP buckets can use either uniform bucket-level access or fine-grained ACLs. Public exposure can come from the bucket IAM policy or from per-object ACLs. This check focuses on the IAM-level bindings, which is where most accidental exposure lives. If your bucket still uses object ACLs, individual objects can be public even when the bucket policy looks clean.


Why it matters

A public bucket is not a theoretical risk. The moment a bucket goes public, it becomes discoverable. Automated scanners constantly enumerate Cloud Storage and S3 namespaces looking for readable buckets, and search engines index public objects. You do not get to rely on the URL being secret.

Here is how this typically plays out:

  • Data exfiltration. If the bucket holds database dumps, application backups, or user uploads, an attacker reads everything. There is no rate limit on damage once the data is copied.
  • Credential leakage. Buckets used for CI artifacts and config frequently contain .env files, service account keys, and API tokens. A leaked key becomes a path deeper into your environment.
  • Object listing. With roles/storage.objectViewer on allUsers, an attacker can list every object name, which maps your data structure for them even before they download anything.
  • Bandwidth and cost abuse. Public read access means anyone can pull objects repeatedly. You pay the egress bill.
  • Compliance failures. PCI DSS, HIPAA, SOC 2, and GDPR all expect access controls on stored data. A public bucket holding regulated data is a reportable finding and, in some cases, a breach.

Warning: allAuthenticatedUsers is often mistaken for "only people in my organization." It is not. It includes every Google account in existence. Treat it as equivalent to public.


How to fix it

The fix has two parts: remove the public bindings, then block them from being re-added. Do both, because removing the binding alone leaves the door open for the next mistake.

Step 1: Confirm what is exposed

Check the current IAM policy on the bucket:

gcloud storage buckets get-iam-policy gs://my-bucket --format=json

Look for bindings containing allUsers or allAuthenticatedUsers. You can also list buckets across a project and grep for them in one pass:

for b in $(gcloud storage buckets list --format="value(name)"); do
  echo "== $b =="
  gcloud storage buckets get-iam-policy "gs://$b" --format=json \
    | grep -E "allUsers|allAuthenticatedUsers" && echo "  PUBLIC BINDING FOUND"
done

Step 2: Remove the public bindings

Danger: If a legitimate public website, static asset host, or CDN origin depends on this bucket, removing the binding will break it immediately. Confirm the bucket is not intentionally serving public content before you run these commands.

Remove the binding for the role that was granted. Repeat for each role you found:

# Remove anonymous read access
gcloud storage buckets remove-iam-policy-binding gs://my-bucket \
  --member=allUsers \
  --role=roles/storage.objectViewer

# Remove "any Google account" read access
gcloud storage buckets remove-iam-policy-binding gs://my-bucket \
  --member=allAuthenticatedUsers \
  --role=roles/storage.objectViewer

If the bucket uses legacy object ACLs rather than uniform access, you also need to strip public ACLs from objects. The cleanest long-term move is to switch to uniform bucket-level access, which disables ACLs entirely:

gcloud storage buckets update gs://my-bucket --uniform-bucket-level-access

Step 3: Turn on public access prevention

This is the part people skip. Public access prevention enforces a hard block, so even if someone tries to add an allUsers binding later, the API rejects it.

gcloud storage buckets update gs://my-bucket --public-access-prevention

Verify it took effect:

gcloud storage buckets describe gs://my-bucket \
  --format="value(iamConfiguration.publicAccessPrevention)"
# Expected output: enforced

Console steps

If you prefer the console:

  1. Open Cloud Storage and select the bucket.
  2. Go to the Permissions tab.
  3. Find the entries for allUsers or allAuthenticatedUsers and remove them.
  4. Under Public access, set Prevent public access to enforced.

Tip: You can enforce public access prevention at the organization or folder level with an org policy, which covers buckets that do not exist yet. See the prevention section below for the constraint to use.


How to prevent it from happening again

Manual cleanup does not scale. The goal is to make a public bucket impossible to create rather than something you find and fix after the fact.

Org policy constraint

Apply the storage.publicAccessPrevention org policy constraint at the organization or folder level. This enforces public access prevention on all current and future buckets in scope:

gcloud resource-manager org-policies enable-enforce \
  storage.publicAccessPrevention \
  --organization=ORGANIZATION_ID

There is also iam.allowedPolicyMemberDomains, which restricts IAM bindings to identities in domains you specify. This blocks allUsers and allAuthenticatedUsers bindings across every resource, not just buckets.

Terraform

If you manage buckets as code, set the controls in the resource definition so drift gets caught on the next plan:

resource "google_storage_bucket" "private" {
  name                        = "my-private-bucket"
  location                    = "US"
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
}

Pair that with a policy-as-code check in CI so a teammate cannot remove those lines without a failing pipeline. A Conftest rule on the Terraform plan does the job:

# Run against a plan converted to JSON
terraform show -json plan.out > plan.json
conftest test plan.json --policy policy/
// policy/storage.rego
package main

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "google_storage_bucket"
  rc.change.after.public_access_prevention != "enforced"
  msg := sprintf("bucket %s must set public_access_prevention=enforced", [rc.address])
}

Tip: Run Lensix on a schedule so any bucket that slips through IaC, gets created by hand, or is provisioned by a third-party tool still surfaces. Org policy plus a continuous check is the combination that actually holds over time.


Best practices

  • Default to private, make public the exception. Buckets should be private unless there is a documented reason for public access, such as a static asset host.
  • Use uniform bucket-level access. Object ACLs are easy to misconfigure and hard to audit. Uniform access gives you one IAM policy to reason about per bucket.
  • Serve public content through a CDN, not a raw bucket. If you need to serve assets publicly, front the bucket with Cloud CDN behind a load balancer and keep the bucket itself private. You get caching, a stable domain, and you avoid exposing the bucket namespace.
  • Grant least privilege. Use specific service accounts and groups with scoped roles instead of broad bindings. allAuthenticatedUsers should never appear in a bucket policy.
  • Turn on data access audit logs. Knowing who read what, and when, is the difference between "we think nothing was accessed" and an actual answer during an incident.
  • Review exceptions regularly. The handful of buckets that are intentionally public should be on a known list and re-justified periodically.

The cheapest public bucket incident is the one a scanner finds before an attacker does. Enforce prevention at the org level, verify with a continuous check, and treat any public binding as something that needs a written reason to exist.