Create a Cloud Storage bucket that cannot accidentally become public
Six steps in the order that makes each one cheap: choose the location type and class first because they are create-time decisions, enable uniform bucket-level access at creation while the 90-day window is irrelevant, turn on public access prevention, grant at the bucket, then verify by attempting the thing you forbade.
markdownguidev198JC-98ZQstage: buildtrust: officialfamily: storageservice: cloud-storageinterface: clicreate bucketuniform bucket-level accesspublic access preventionallUsersgcloud storage buckets createbucket locationstorage classsigned URL
A bucket that cannot accidentally become public
Step 1 — decide the location type, because it is a create-time decision
zone (only with the RAPID class), region, dual-region or multi-region. Decide against the data's
residency requirement and its readers' geography, and decide now: the location is part of what you create,
not something you reconfigure afterwards.
Step 2 — decide the class, knowing what it commits
STANDARD is the default. NEARLINE, COLDLINE and ARCHIVE carry minimum storage durations of 30, 90 and
365 days and all three carry retrieval fees. Pick the class the data's actual access pattern justifies, and
check it against any retention or deletion rule you are also about to write
(reference/storage/cloud-storage.md §2).
Step 3 — enable uniform bucket-level access at creation
This is the step the whole guide exists for. Uniform bucket-level access "allows you to use Identity and Access Management (IAM) alone to manage permissions", and Google marks it "(recommended)" because fine-grained access brings "an increased chance of unintentional data exposure".
Doing it at creation also sidesteps the one-way door: you have 90 days to switch back "before uniform
bucket-level access becomes permanent", and on a brand-new bucket with no consumers that window costs nothing.
On an inherited bucket it is a 90-day clock on an unproven change
(reference/storage/cloud-storage-access-control.md §3).
Step 4 — turn on public access prevention
It blocks allUsers and allAuthenticatedUsers. Note what the second one is not: it is not
"authenticated to my organization", it is any authenticated Google account anywhere. Most accidental public
buckets are that misreading.
Step 5 — grant at the bucket, not at the project
Bind the predefined role the caller actually needs on the bucket — roles/storage.objectViewer to read
objects, not roles/storage.admin on the project. Inheritance runs downward, so a project-level grant reaches
every bucket you will ever create there (reference/identity/iam-model.md §2).
If a browser or a third party needs one object, that is a signed URL, not a grant and never a public bucket.
Step 6 — verify by attempting the thing you forbade
A configuration you have not tried is a belief. Make these three calls:
| Attempt | Expected |
|---|---|
| read an object as an unauthenticated caller | 401 required or 403 forbidden — never a 200 |
| set an object ACL | 400 badRequest — "The request cannot be completed based on your current Cloud Storage settings". That 400 is uniform bucket-level access working |
| read an object as the intended workload identity | 200 |
If the first returns 200 the bucket is public; stop and fix it before anything else is written to it. If the second returns 200, uniform bucket-level access is not on.
For a migration
Every bucket created this way removes a class of finding from the security review on the far side, and the two
settings in steps 3 and 4 have no per-object equivalent to audit afterwards. That is the cheapest hour in a
storage migration. The counterpart settings on AWS (S3 Block Public Access, bucket owner enforced) and Azure
(disallow public blob access) are in guides/parity/storage.md.