Cloud Storage Security and Static Sites
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Commands and behaviour below are current for the gcloud CLI 586.x line (gcloud storage command group), the Cloud
Storage documentation and release notes published through 2026-09-30, the Google Cloud Client Libraries for Java
(libraries-bom) and Terraform hashicorp/google v8 (checked 2026-10-01). Cloud Storage has changed its defaults
repeatedly since 2023 (soft delete on by default, uniform bucket-level access and domain-restricted sharing enforced
in new organizations, gcloud storage replacing gsutil), so older guides that make a bucket public with object ACLs
or sign URLs with a downloaded key file no longer work as written. Verify flags with gcloud storage --help and the
linked reference pages.
This page is the security half of Cloud Storage. The basics — buckets, objects, storage classes, locations,
versioning, soft delete, lifecycle rules, transfers and Cloud Storage FUSE — are in
Cloud Storage, and the caching layer in front of the static site is in
Cloud CDN and Media CDN. Two Bookshelf buckets are used throughout:
bookshelf-web-$PROJECT_ID (the single page application, served to the internet through a global external
Application Load Balancer) and bookshelf-uploads-$PROJECT_ID (user uploads written by catalog-api, private,
read back through signed URLs). The AWS and Azure counterparts are S3 Security and Static Hosting
and Blob Storage; the Spring Boot side of file storage is in
File Storage & Object Stores; the React build that produces the static site is in
Build and Deployment.
The security model in one page
Four independent layers decide whether a request succeeds, and knowing which one produced a 403 is most of
troubleshooting: organization policy (constraints such as storage.uniformBucketLevelAccess,
storage.publicAccessPrevention and iam.allowedPolicyMemberDomains, several enforced in organizations created from
2024-05-03); public access prevention (a switch that rejects allUsers and allAuthenticatedUsers); IAM, with
optional conditions, as the only mechanism once ACLs are off; and encryption plus VPC Service Controls (the caller
must be allowed to use the key and be inside the perimeter).
The remaining sections follow that order. Everything that calls the API uses Application Default Credentials: the
service account attached to Cloud Run, the Workload Identity Federation principal of a pipeline, or
--impersonate-service-account on a developer machine (see Identity and Access).
No page example downloads a service-account key.
Baseline: private buckets with uniform access
A bucket created through the console today already follows most of the guidance below, and in a secure-by-default organization the rest is enforced for you. The settings to keep are:
| Control | Recommended value | Notes |
|---|---|---|
Uniform bucket-level access (UBLA) |
On |
Disables ACLs for the bucket and its objects, so IAM is the single access system. Enforced by the
|
Public access prevention |
|
Set on the bucket or through the |
Default encryption |
Google-managed key, or CMEK where policy requires it |
All data is encrypted at rest; the choice is who controls the key (below). |
Older guides (and gsutil acl ch -u AllUsers:R) make objects public with ACLs. That requires UBLA to be off and PAP
to be off, and both are now blocked in new organizations. The current approach is IAM bindings on the bucket, and for
anything public, a deliberate exception described in Hosting a static website.
In the Google Cloud console
-
Pick the project in the project picker and open Cloud Storage → Buckets (open Cloud Shell from the toolbar if you prefer to run the CLI below). If the API is disabled the console offers to enable it.
-
Choose Create, enter the globally unique name
bookshelf-uploads-<project-id>, and select a Region (us-central1) as the location type. -
For Choose a storage class, keep Standard (see Cloud Storage for the others).
-
For Choose how to control access to objects, keep Enforce public access prevention on this bucket checked and Uniform selected.
-
For Choose how to protect object data, leave Soft delete policy at the default, tick Object versioning if needed, and choose the encryption key (Google-managed or Cloud KMS key), then choose Create.
With the gcloud CLI
gcloud services enable storage.googleapis.com iam.googleapis.com iamcredentials.googleapis.com \
cloudkms.googleapis.com compute.googleapis.com certificatemanager.googleapis.com
export PROJECT_ID=$(gcloud config get-value project) # placeholder: my-project-id
export REGION=us-central1
export WEB_BUCKET=bookshelf-web-$PROJECT_ID
export UPLOADS_BUCKET=bookshelf-uploads-$PROJECT_ID
# Private bucket for uploads: UBLA on, public access blocked
gcloud storage buckets create gs://$UPLOADS_BUCKET \
--location=$REGION \
--default-storage-class=STANDARD \
--uniform-bucket-level-access \
--public-access-prevention
# Verify
gcloud storage buckets describe gs://$UPLOADS_BUCKET \
--format="yaml(uniform_bucket_level_access,public_access_prevention,soft_delete_policy)"
Derived from gcloud storage buckets create and gcloud storage buckets describe.
The same baseline in Terraform, which is what the Bookshelf baseline module should contain:
resource "google_storage_bucket" "uploads" {
name = "bookshelf-uploads-${var.project_id}"
location = "US-CENTRAL1"
uniform_bucket_level_access = true
public_access_prevention = "enforced"
versioning {
enabled = true
}
soft_delete_policy {
retention_duration_seconds = 604800 # 7 days
}
}
Derived from the google_storage_bucket resource reference and the uniform bucket-level access page.
IAM roles for Cloud Storage
Grant roles to the narrowest principal at the narrowest scope: a bucket (or a managed folder) rather than the project. The predefined roles you will use are:
| Role | Grants |
|---|---|
|
List objects and read object data and metadata. This is the role for readers and, bound to |
|
Create objects only (no read, overwrite or delete). Useful for write-only ingest. |
|
Read, write, list and delete objects, but not change IAM policies or bucket settings. The usual role for an
application such as |
|
Everything on objects including setting object ACLs (ignored under UBLA) and object holds. |
|
Full control of buckets and objects, including IAM policies. Keep it for the pipeline that provisions buckets, not for applications. |
|
Manage managed folders and the objects under them (see below). |
Basic roles (Owner, Editor, Viewer) also contain Cloud Storage permissions at project level, and the legacy
bucket roles (roles/storage.legacyBucketReader and friends) are added automatically to old buckets; neither belongs
in a new design. Prefer the roles above. The full role list is in the
Cloud Storage IAM roles reference.
In the Google Cloud console
-
Open Cloud Storage → Buckets → select
bookshelf-uploads-<project-id>→ Permissions. -
Choose Grant access, enter the service account
catalog-api-sa@<project-id>.iam.gserviceaccount.comas the new principal and select the role Storage Object User. -
To restrict the grant to a prefix, choose Add IAM condition (Condition → Edit), give it a title and use the condition editor with the expression shown in the CLI section, then choose Save.
With the gcloud CLI
export SA=catalog-api-sa@$PROJECT_ID.iam.gserviceaccount.com
# Read and write the whole bucket
gcloud storage buckets add-iam-policy-binding gs://$UPLOADS_BUCKET \
--member="serviceAccount:$SA" \
--role=roles/storage.objectUser
# Read only objects under uploads/public-covers/ (IAM Condition on the resource name)
gcloud storage buckets add-iam-policy-binding gs://$UPLOADS_BUCKET \
--member="serviceAccount:notifier-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--role=roles/storage.objectViewer \
--condition='title=covers-only,description=Read cover images only,expression=resource.name.startsWith("projects/_/buckets/'"$UPLOADS_BUCKET"'/objects/uploads/public-covers/")'
# Inspect the policy
gcloud storage buckets get-iam-policy gs://$UPLOADS_BUCKET --format=yaml
Derived from gcloud storage buckets add-iam-policy-binding and the IAM Conditions on buckets page.
Resource names in conditions have the form projects//buckets/BUCKET/objects/OBJECT; the is literal. Conditional
bindings only work on buckets with uniform bucket-level access, and a policy that contains conditions must be read and
written with version 3 (the output shows version: 3).
Managed folders
Managed folders are resources inside a bucket with their own IAM policy, so one prefix can be shared without conditions or a new bucket. They require UBLA and apply to every object below the prefix.
gcloud storage managed-folders create gs://$UPLOADS_BUCKET/exports/
gcloud storage managed-folders add-iam-policy-binding gs://$UPLOADS_BUCKET/exports/ \
--member="group:report-readers@example.com" --role=roles/storage.objectViewer
Derived from gcloud storage managed-folders create and the Managed folders page. In the console, open the bucket → Create folder → managed folder, then Permissions on it.
Public access prevention and domain-restricted sharing
Public access prevention stops accidental exposure. When it is enforced on a bucket, a request to add allUsers or
allAuthenticatedUsers fails and existing public bindings stop working; when it is inherited, the organization
policy constraints/storage.publicAccessPrevention decides. Treat a public bucket as the exception, reviewed and
documented, and keep every other bucket at enforced.
A second control is easy to miss. Organizations created from 2024-05-03 enforce
constraints/iam.allowedPolicyMemberDomains (domain-restricted sharing), which rejects any IAM binding whose
principal is outside your organization — including allUsers. Even with PAP off, granting objectViewer to
allUsers then fails with a policy error. Options are to exempt a specific project or folder by tag or by a narrower
policy set by an organization administrator, or not to make the bucket public at all and serve the site with signed
URLs or cookies through Cloud CDN (see Cloud CDN and Media CDN). See
Resource Hierarchy and Projects for how policies inherit.
In the Google Cloud console
Open the bucket → Permissions → Public access, then choose Prevent public access, or Remove public access
prevention on the web bucket only. Administrators set the organization-wide switch under IAM & Admin →
Organization Policies → storage.publicAccessPrevention.
With the gcloud CLI
# Enforce on an existing bucket
gcloud storage buckets update gs://$UPLOADS_BUCKET --public-access-prevention
# Allow public IAM on the web bucket only (lifts PAP back to inherited)
gcloud storage buckets update gs://$WEB_BUCKET --no-public-access-prevention
# Organization-wide (needs roles/orgpolicy.policyAdmin on the organization)
gcloud resource-manager org-policies enable-enforce storage.publicAccessPrevention \
--organization=<organization-id>
Derived from
gcloud storage buckets update and the
Public access prevention page. The
org-policies command group is the long-standing way to set a boolean constraint; for new automation Google
recommends gcloud org-policies set-policy with a policy file.
Signed URLs (V4) without keys
A signed URL gives anyone holding it temporary access to one object, with the permissions of the account that signed
it, without a Google identity. Bookshelf uses them so the browser downloads a user’s upload (or uploads a cover)
directly from Cloud Storage instead of streaming it through catalog-api.
Version 4 signing (HMAC is for interoperability, below) has these rules:
-
The URL expires after the requested duration, at most 7 days (604800 seconds) when signed with a service account through the API. With the gcloud CLI the 7-day maximum applies to service-account signing; a command signed with user credentials is limited to a shorter validity.
-
The signer must itself have the permission the URL grants: a URL for
GETsigned by an account that cannot read the object fails with403when used. -
Older guides download a service-account JSON key and pass
--private-key-file. That works but is blocked whereiam.disableServiceAccountKeyCreationis enforced (the default in new organizations). The current approach is to let the IAM Credentials API sign on behalf of the service account: the caller needsroles/iam.serviceAccountTokenCreatoron the signing service account (a service account may be granted it on itself), and no key ever exists.
With the gcloud CLI
# One-time: let the developer or pipeline identity sign as the service account
gcloud iam service-accounts add-iam-policy-binding $SA \
--member="user:developer@example.com" \
--role=roles/iam.serviceAccountTokenCreator
# Download link valid for 15 minutes
gcloud storage sign-url gs://$UPLOADS_BUCKET/uploads/book-42/cover.png \
--duration=15m \
--impersonate-service-account=$SA
# Upload link: the client must send the same Content-Type header
gcloud storage sign-url gs://$UPLOADS_BUCKET/uploads/book-42/new-cover.png \
--http-verb=PUT \
--headers=content-type=image/png \
--duration=10m \
--impersonate-service-account=$SA
Derived from gcloud storage sign-url and the Signed URLs page. In the console there is no "create signed URL" action for objects; the console’s Share publicly action is something different and requires public access.
In Java
On Cloud Run the library obtains Compute Engine credentials from the metadata server, and those credentials can sign
through the IAM Credentials API, so no key file is needed. Locally, use impersonated credentials (set up with
gcloud auth application-default login --impersonate-service-account=$SA).
package com.example.bookshelf.upload;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.HttpMethod;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.Storage.SignUrlOption;
import com.google.cloud.storage.StorageOptions;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class UploadLinks {
private final Storage storage = StorageOptions.getDefaultInstance().getService();
private final String bucket;
public UploadLinks(String bucket) {
this.bucket = bucket;
}
/** Link the browser can PUT the file to; it must send the same Content-Type. */
public URL uploadUrl(String objectName, String contentType) {
BlobInfo blob = BlobInfo.newBuilder(BlobId.of(bucket, objectName))
.setContentType(contentType)
.build();
return storage.signUrl(blob, 10, TimeUnit.MINUTES,
SignUrlOption.httpMethod(HttpMethod.PUT),
SignUrlOption.withExtHeaders(Map.of("Content-Type", contentType)),
SignUrlOption.withV4Signature());
}
/** Link the browser can GET to download the file. */
public URL downloadUrl(String objectName) {
BlobInfo blob = BlobInfo.newBuilder(BlobId.of(bucket, objectName)).build();
return storage.signUrl(blob, 15, TimeUnit.MINUTES, SignUrlOption.withV4Signature());
}
}
Derived from the
V4 signed URL code sample, the
signed POST policy sample
and the Cloud Storage client libraries page. The
service account needs roles/iam.serviceAccountTokenCreator on itself so that signBlob is allowed. Add the Cloud
Storage client to the build through the com.google.cloud:libraries-bom and the google-cloud-storage artifact (or
the Spring Cloud GCP Storage starter, see File Storage & Object Stores).
A signed POST policy (storage.generateSignedPostPolicyV4) is the form-based variant: the server signs a policy
listing the object name and constraints, and the browser submits a multipart form directly to Cloud Storage. Use it
for plain HTML forms and a signed PUT URL for programmatic clients. Because
the URL is a bearer credential, keep lifetimes short, never log it, and scope it to one object.
HMAC keys for S3-interoperable clients
Tools that only speak the Amazon S3 API can call the XML API at storage.googleapis.com with an HMAC key tied to a
service account: gcloud storage hmac create $SA prints the access ID and secret once (store the secret in
Secret Manager and Cloud KMS), and hmac update --deactivate and hmac delete retire it.
An HMAC key is a long-lived secret like a service-account key, so use it only when a client cannot use OAuth. See
gcloud storage hmac create and the
HMAC keys page.
Encryption: Google-managed keys, CMEK and CSEK
Every object is encrypted at rest with no action on your part. The three ways to control the key are:
| Option | What it means |
|---|---|
Google-managed encryption key (default) |
Google creates, rotates and stores the keys. No setup, no extra cost. |
Customer-managed encryption key (CMEK) |
You create a key in Cloud KMS (optionally HSM or external) and set it as the bucket default or per object. You control rotation, can disable the key to make the data unreadable, and get audit logs of each use. The bucket and the key must be in a compatible location. |
Customer-supplied encryption key (CSEK) |
You send your own AES-256 key with every request and Google never stores it. Operationally heavy and not supported by features such as some transfer tools; use CMEK unless a rule requires CSEK. |
For CMEK the bucket’s service agent needs roles/cloudkms.cryptoKeyEncrypterDecrypter on the key; the CLI creates the
agent and grants the role in one command.
In the Google Cloud console
-
Open Security → Key management and create a key ring (
bookshelf, locationus-central1) and a key (uploads, purpose Symmetric encrypt/decrypt, rotation as required). -
Open Cloud Storage → Buckets → the bucket → Configuration → Encryption type → Edit and choose Cloud KMS key, select the key, and accept the prompt to grant the service agent access.
With the gcloud CLI
gcloud kms keyrings create bookshelf --location=$REGION
gcloud kms keys create uploads --keyring=bookshelf --location=$REGION --purpose=encryption \
--default-algorithm=google-symmetric-encryption
export KEY=projects/$PROJECT_ID/locations/$REGION/keyRings/bookshelf/cryptoKeys/uploads
# Create the Cloud Storage service agent if needed and let it use the key
gcloud storage service-agent --project=$PROJECT_ID --authorize-cmek=$KEY
# Use it for new objects in the bucket (existing objects keep their key until rewritten)
gcloud storage buckets update gs://$UPLOADS_BUCKET --default-encryption-key=$KEY
# Re-encrypt an existing object with the new key
gcloud storage objects update gs://$UPLOADS_BUCKET/uploads/book-42/cover.png --encryption-key=$KEY
Derived from
gcloud kms keys create,
gcloud storage service-agent and the
Customer-managed encryption keys
page. Key creation, rotation and destruction are covered in Secret Manager and Cloud KMS.
If the key is disabled or destroyed, reads return an error and the data is unrecoverable once the key version is
destroyed, which is why key-version destruction is scheduled rather than immediate. The organization policy
constraints/gcp.restrictNonCmekServices can require CMEK for Cloud Storage.
Retention policies, holds and object retention
When data must not change or disappear for a period, use retention rather than permissions alone.
| Feature | Behaviour |
|---|---|
Bucket retention policy |
Every object in the bucket cannot be deleted or replaced until it is at least the retention period old. Locking the policy (bucket lock) is irreversible: the period can only be extended and the bucket cannot be deleted until its objects age out. |
Object holds |
A temporary hold or an event-based hold on a single object blocks deletion until released. A default event-based hold makes new objects start held; releasing the hold starts the retention clock. |
Object Retention Lock |
A per-object retain-until date and mode ( |
Soft delete and versioning |
Recovery features, not protection: soft delete is configurable per bucket and can be shortened, versioning keeps noncurrent versions until a lifecycle rule removes them. |
In the Google Cloud console
Open the bucket → Protection, choose Set retention policy, enter a duration and optionally Lock it; for one object, select it → Edit protection and set a hold or retention.
With the gcloud CLI
# Retention: nothing in the bucket can be deleted for 30 days
gcloud storage buckets update gs://$UPLOADS_BUCKET --retention-period=30d
# Hold one object
gcloud storage objects update gs://$UPLOADS_BUCKET/uploads/book-42/cover.png --temporary-hold
gcloud storage objects update gs://$UPLOADS_BUCKET/uploads/book-42/cover.png --no-temporary-hold
# Per-object retention needs the feature on the bucket at creation time
gcloud storage buckets create gs://bookshelf-archive-$PROJECT_ID --location=$REGION \
--uniform-bucket-level-access --public-access-prevention --enable-per-object-retention
gcloud storage objects update gs://bookshelf-archive-$PROJECT_ID/invoice-2026.pdf \
--retain-until=2027-12-31T00:00:00Z --retention-mode=Unlocked
Derived from
gcloud storage buckets update,
gcloud storage objects update and the
Retention policies and bucket lock and
Object Retention Lock pages. Locking a retention policy
(gcloud storage buckets update --lock-retention-period) is permanent, so do it only on a real data bucket after
review and never on the lab buckets of this page.
CORS
Browsers block cross-origin reads unless the server answers with CORS headers. When the Bookshelf front end at
https://www.example.com loads fonts or JSON directly from a bucket, or uploads to a signed URL with fetch, the
bucket needs a CORS configuration that names the origin, the methods and the response headers the page may read.
[
{
"origin": ["https://www.example.com"],
"method": ["GET", "PUT", "HEAD"],
"responseHeader": ["Content-Type", "x-goog-resumable"],
"maxAgeSeconds": 3600
}
]
gcloud storage buckets update gs://$UPLOADS_BUCKET --cors-file=cors.json
gcloud storage buckets describe gs://$UPLOADS_BUCKET --format="yaml(cors_config)"
# Remove it
gcloud storage buckets update gs://$UPLOADS_BUCKET --clear-cors
Derived from
gcloud storage buckets update and the
Configure cross-origin resource sharing (CORS) page. In the
console the bucket’s CORS cannot be edited; use the CLI, Terraform (cors block) or the API. CORS applies to the
XML API and to the JSON API calls the browser makes; it is not applied to requests served through the load balancer
for the static site, where headers are set on the backend bucket (custom response headers) instead, see
Cloud CDN and Media CDN. Keep origins explicit; "*" is rarely justified.
Requester pays
With requester pays the caller’s project, not the owner’s, is billed for transfer and operations, and every request
must name a billing project (gcloud storage buckets update gs://BUCKET --requester-pays, then
gcloud storage ls gs://BUCKET --billing-project=$PROJECT_ID). It suits shared datasets, does not work with
anonymous access or signed URLs, and does not combine with the public site below. See the
Requester Pays page.
VPC Service Controls
IAM answers "who may call"; VPC Service Controls answers "from where". A perimeter around the project makes Cloud Storage reject calls whose source is outside the perimeter or outside an allowed access level, even with valid credentials, which limits data exfiltration with stolen tokens or misconfigured IAM. Create the perimeter in dry-run mode first and read the violations in the audit logs before enforcing.
gcloud access-context-manager perimeters dry-run create bookshelf_perimeter --policy=<access-policy-id> \
--perimeter-title="Bookshelf" --perimeter-type=regular \
--perimeter-resources=projects/<project-number> --perimeter-restricted-services=storage.googleapis.com
Derived from gcloud access-context-manager perimeters dry-run create and the VPC Service Controls overview. Perimeters are created at the organization level and need Access Context Manager permissions; see Governance, Security and Compliance for the governance context. A perimeter also blocks a browser downloading through a signed URL from outside it unless an ingress rule or access level allows it, which is why the Bookshelf static bucket is normally kept outside the perimeter that guards the uploads bucket.
Hosting a static website
Cloud Storage can serve a site directly from a bucket, but with two limits that decide the design:
-
The bucket-hostname form (
https://storage.googleapis.com/BUCKET/index.html, or a CNAME toc.storage.googleapis.com) serves HTTP only on a custom domain; Cloud Storage does not terminate TLS for your own domain. -
The site content must be readable by everyone, so the bucket needs
allUserswithroles/storage.objectViewer, public access prevention off, and an organization that permits a public principal.
The current approach for a production site is therefore the global external Application Load Balancer with a backend bucket, a Google-managed certificate from Certificate Manager and, optionally, Cloud CDN on the backend bucket. The bucket stays the origin and the load balancer provides HTTPS, the anycast IP address, HTTP-to-HTTPS redirect and Cloud Armor if needed. Firebase Hosting is the simpler alternative for single page applications because it includes TLS, a CDN, rewrite rules and preview channels without a load balancer, at the price of a separate product and deploy tool.
The bucket website settings still matter behind the load balancer: MainPageSuffix is the file served for a
directory-style request such as / and NotFoundPage is the object returned with a 404 status for a missing path.
A single page application whose router uses real paths needs unknown paths to return index.html; setting
NotFoundPage to index.html makes the app load but the status code stays 404, so use hash routing, a URL-map
rewrite to the app shell, or Firebase Hosting rewrites when the status matters to crawlers.
In the Google Cloud console
-
Open Cloud Storage → Buckets → Create, name it
bookshelf-web-<project-id>inus-central1(or a multi-region), and leave Uniform access. On the access page, uncheck Enforce public access prevention for this bucket only. -
Upload the build output of the React app (the
distfolder) with Upload → Upload folder. -
Open the bucket → Permissions → Grant access, principal
allUsers, role Storage Object Viewer, and confirm that the content is public. -
Open the bucket → Configuration → Edit website configuration and set Index (main) page to
index.htmland Error (404) page toindex.htmlor404.html. -
Open Network services → Load balancing → Create load balancer → Application Load Balancer (HTTP/S) → From Internet to my VMs or serverless services → Global external Application Load Balancer.
-
In Backend configuration create a Backend bucket pointing to the web bucket and tick Enable Cloud CDN.
-
In Frontend configuration choose protocol HTTPS, create a new global IPv4 address, and create a certificate with Certificate Manager for
www.example.com. -
Review and choose Create. Create an
Arecord forwww.example.compointing to the new address in your DNS provider and wait until the certificate status is Active.
With the gcloud CLI
export DOMAIN=www.example.com # placeholder; use a domain you control
# 1. The bucket: public by exception (UBLA on, PAP off for this bucket only)
gcloud storage buckets create gs://$WEB_BUCKET \
--location=$REGION --uniform-bucket-level-access --no-public-access-prevention
gcloud storage buckets update gs://$WEB_BUCKET \
--web-main-page-suffix=index.html --web-error-page=index.html
gcloud storage cp --recursive dist/* gs://$WEB_BUCKET/
gcloud storage buckets add-iam-policy-binding gs://$WEB_BUCKET \
--member=allUsers --role=roles/storage.objectViewer
# 2. Global address and backend bucket with Cloud CDN
gcloud compute addresses create bookshelf-web-ip --global --ip-version=IPV4
gcloud compute backend-buckets create bookshelf-web-backend \
--gcs-bucket-name=$WEB_BUCKET --enable-cdn
# 3. URL map
gcloud compute url-maps create bookshelf-web-map --default-backend-bucket=bookshelf-web-backend
# 4. Google-managed certificate through a certificate map (DNS must point to the address first)
gcloud certificate-manager certificates create bookshelf-cert --domains=$DOMAIN
gcloud certificate-manager maps create bookshelf-map
gcloud certificate-manager maps entries create bookshelf-entry \
--map=bookshelf-map --hostname=$DOMAIN --certificates=bookshelf-cert
# 5. HTTPS proxy and forwarding rule
gcloud compute target-https-proxies create bookshelf-https-proxy \
--url-map=bookshelf-web-map --certificate-map=bookshelf-map
gcloud compute forwarding-rules create bookshelf-https-rule \
--global --load-balancing-scheme=EXTERNAL_MANAGED --network-tier=PREMIUM \
--address=bookshelf-web-ip --target-https-proxy=bookshelf-https-proxy --ports=443
# 6. Show the address for the DNS A record
gcloud compute addresses describe bookshelf-web-ip --global --format='value(address)'
Derived from gcloud compute backend-buckets create, gcloud compute url-maps create, gcloud certificate-manager certificates create, gcloud compute target-https-proxies create, gcloud compute forwarding-rules create and the Host a static website page. A Certificate Manager certificate for a domain that is not yet pointing at the load balancer stays provisioning; the managed certificate with load-balancer authorization or DNS authorization is described in the Certificate Manager overview and step-by-step in Cloud CDN and Media CDN, which also adds the HTTP-to-HTTPS redirect, cache modes and invalidation.
A bucket that stays private cannot be read by a plain backend bucket. If domain-restricted sharing or company policy
forbids allUsers, keep the bucket private, enable Cloud CDN signed URLs or signed cookies on the backend bucket and
issue them from catalog-api, which also lets you gate the site behind a login (see the CDN page).
Firebase Hosting alternative
firebase init hosting followed by firebase deploy --only hosting publishes the dist folder with TLS and a CDN
(derived from the Firebase Hosting quickstart). It serves from
its own origin, not a bucket you manage, so the bucket controls on this page do not apply to it.
Auditing and monitoring
Admin Activity audit logs (bucket and IAM changes) are always on and free. Data Access audit logs, which record who
read or wrote which object, are off by default for Cloud Storage because they can be voluminous, and they are billed as
log ingestion. Enable DATA_READ and DATA_WRITE for storage.googleapis.com on sensitive buckets only, and route
them to a log bucket or BigQuery sink.
# Add to the project IAM policy (gcloud projects get-iam-policy, edit, set-iam-policy)
auditConfigs:
- service: storage.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
gcloud logging read \
'protoPayload.serviceName="storage.googleapis.com" AND logName:"cloudaudit.googleapis.com%2Fdata_access"' \
--limit=5 --format="table(timestamp,protoPayload.methodName,protoPayload.resourceName)"
Derived from the Cloud Storage audit logging page and gcloud logging read. In the console use IAM & Admin → Audit Logs, filter for Google Cloud Storage, and tick Data Read and Data Write. Also look at the Security Command Center findings for public buckets, covered in Governance, Security and Compliance.
Retired and legacy features to avoid
-
Object ACLs and bucket ACLs are replaced by IAM and UBLA;
gsutil acland--predefined-acldo not work on UBLA buckets. -
gsutilis legacy and minimally maintained, lacks soft delete and managed folders, and after March 2027 stops shipping with the gcloud CLI. Usegcloud storage. -
Signing with a downloaded key file (
gsutil signurl -p) conflicts with key-creation policy; sign through IAMsignBlobas above. V2 signed URLs are legacy; use V4. -
Object change notification (watching a bucket with
gsutil notification watchbucket) was deprecated on 2026-01-30; use Pub/Sub notifications or Eventarc (see Cloud Run Functions and Events). -
Launch stages. Soft delete has been on by default (7 days) since March 2024; secure-by-default organization policies apply from 2024-05-03; Rapid Cache (formerly Anywhere Cache, renamed 2026-03-24) and Storage Intelligence (GA 2026-09-10) are newer; check the release notes before relying on a Preview feature.
-
Public buckets with HTTP-only CNAME hosting for a production site; put the bucket behind the load balancer.
Scaling and limits
A single object name accepts roughly one write per second and bucket metadata or IAM updates are rate limited, so do not use one object as a counter. V4 signed URLs live at most 7 days, and bucket IAM policies, CORS and lifecycle configurations have size limits, so prefer groups and managed folders over thousands of individual bindings (see the quotas and limits page for current numbers). Behind the load balancer, Cloud CDN absorbs repeat reads, and forwarding-rule, proxy and URL-map quotas apply per project.
Pricing model
Prices change; use the Cloud Storage pricing page and the Google Cloud Pricing Calculator and note the date you looked. The security features on this page mostly add small cost drivers:
-
Cloud Storage itself. UBLA, PAP, IAM, IAM Conditions, managed folders, signed URLs, CORS and requester-pays settings have no extra charge beyond storage, operations and network egress. Retention and holds keep bytes you would otherwise delete, and soft-deleted and noncurrent bytes are billed as storage.
-
CMEK. Cloud KMS charges per active key version and per cryptographic operation (more for HSM and external keys); object reads and writes with CMEK each call KMS.
-
Auditing. Admin Activity logs are free; Data Access logs are billed as Cloud Logging ingestion and storage.
-
Static hosting. The global external Application Load Balancer charges per forwarding rule and per GB processed; Cloud CDN charges cache egress and cache fill, and misses are billed as Cloud Storage egress to the load balancer. Firebase Hosting has its own free allowance and then storage and transfer charges.
-
Free tier. The Always Free tier includes a small amount of Standard storage and operations in selected US regions each month; it does not cover load balancing, KMS or logging.
Troubleshooting 403 and related errors
| Symptom | Likely cause and fix |
|---|---|
|
The signing account lacks the permission, or the URL expired. Check the signer’s role on the object and the
|
|
A signed header differs from the one sent (usually |
|
Grant |
Error that ACLs cannot be used with uniform bucket-level access |
UBLA is on. Remove the ACL flag and grant an IAM role instead. |
Cannot add |
PAP is enforced or |
Reads fail on a CMEK bucket |
The key is disabled, in the wrong location, or the Cloud Storage service agent lacks
|
Browser reports a CORS error |
No matching |
Site shows |
The objects are not public (missing |
Site returns |
The object does not exist; set |
HTTPS certificate stuck in provisioning |
DNS does not yet resolve to the load balancer address, or the certificate map entry is missing. |
Clean up
Delete in dependency order, in a throwaway project where possible. A bucket with a locked retention policy cannot be deleted until its objects age out, and soft-deleted objects remain recoverable (and billed) until their window ends.
# 1. Load balancer chain: forwarding rule, proxy, certificate map, URL map, backend bucket, address
gcloud compute forwarding-rules delete bookshelf-https-rule --global --quiet
gcloud compute target-https-proxies delete bookshelf-https-proxy --quiet
gcloud certificate-manager maps entries delete bookshelf-entry --map=bookshelf-map --quiet
gcloud certificate-manager maps delete bookshelf-map --quiet
gcloud certificate-manager certificates delete bookshelf-cert --quiet
gcloud compute url-maps delete bookshelf-web-map --quiet
gcloud compute backend-buckets delete bookshelf-web-backend --quiet
gcloud compute addresses delete bookshelf-web-ip --global --quiet
# 2. Buckets, with every object version
gcloud storage rm --recursive gs://$WEB_BUCKET gs://$UPLOADS_BUCKET gs://bookshelf-archive-$PROJECT_ID
# 3. Schedule destruction of the CMEK key version (key rings cannot be deleted)
gcloud kms keys versions destroy 1 --key=uploads --keyring=bookshelf --location=$REGION
Derived from
gcloud storage rm,
gcloud compute forwarding-rules delete
and gcloud kms keys versions destroy.
gcloud storage rm --recursive on a bucket removes the objects, every noncurrent version and the bucket itself;
a bucket still under retention fails until the retention period of each object has passed. Soft-deleted objects can be
listed with gcloud storage ls --soft-deleted and are purged when the soft-delete window ends. Key-version
destruction is scheduled (24 hours by default) and the key ring and key resources remain, so no further charge
accrues once the versions are destroyed. If the Firebase site was used, run firebase hosting:disable. Deleting the
whole throwaway project removes everything at once (see Resource Hierarchy and Projects),
after a 30-day recovery window.
Related pages
-
Cloud Storage covers buckets, storage classes, versioning, lifecycle, transfers and Cloud Storage FUSE.
-
Cloud CDN and Media CDN covers the load balancer, Cloud CDN, cache modes, signed cookies, invalidation and certificates in depth.
-
Secret Manager and Cloud KMS covers Cloud KMS keys, rotation and Secret Manager.
-
Identity and Access covers roles, service accounts, impersonation and Workload Identity Federation.
-
Governance, Security and Compliance covers audit logs, organization policies and Security Command Center.
-
Resource Hierarchy and Projects covers how organization policies inherit.
-
S3 Security and Static Hosting and Blob Storage are the AWS and Azure counterparts.
-
File Storage & Object Stores and Build and Deployment show the application and front-end sides.