Environment details
- Programming language: Python 3.11
- Package:
google-cloud-storage
- Versions observed:
3.11.0 and 3.12.0 (the BucketMetadataCache / ACO code path is present in both)
Summary
Client.__init__ unconditionally instantiates a BucketMetadataCache ("App-centric Observability" / ACO). As a side effect, ordinary object-level operations (list_blobs, download_*, upload_*, etc.) now trigger a background Client.get_bucket() call, i.e. the REST storage.buckets.get permission — even though the application code never requests bucket metadata and the operation itself only needs storage.objects.*.
For principals granted object-only roles (roles/storage.objectViewer, roles/storage.objectAdmin) — which intentionally exclude storage.buckets.get — this produces a continuous stream of denied storage.buckets.get entries in Cloud Audit Logs (data_access, severity ERROR), one per bucket per client instance.
The failure is swallowed internally (data operations still succeed), so it is not a functional outage — but it is:
- Alarming, high-volume audit-log noise (ERROR severity, "lots per second" under fan-out workloads that create many short-lived clients), and
- Pressure to over-grant IAM — teams are asked to add
storage.buckets.get (e.g. via roles/storage.bucketViewer/legacyBucketReader) purely to satisfy a telemetry probe the application does not use, across every bucket the service account ever touches. This directly conflicts with least-privilege.
There is no supported way to disable it — no client option and no environment variable (the only observability env var, ENABLE_GCS_PYTHON_CLIENT_OTEL_TRACES, is a separate opt-in for OpenTelemetry tracing). The only workaround is mutating the private attribute client._bucket_metadata_cache = None, which is fragile and fails open on library upgrades.
Steps to reproduce
Object-only operations cause a background get_bucket() (→ storage.buckets.get). The repro below is fully offline and needs no credentials or real bucket — it monkeypatches get_bucket only to observe that it is invoked and by whom:
import time
import traceback
from google.auth.credentials import AnonymousCredentials
from google.cloud import storage
calls = []
def traced_get_bucket(self, bucket_name, *a, **k):
if not calls:
print("get_bucket() caller:\n" + "".join(traceback.format_stack()[-3:-1]))
calls.append(bucket_name)
raise RuntimeError("short-circuit (offline repro)") # swallowed by ACO bg thread
storage.Client.get_bucket = traced_get_bucket
client = storage.Client(project="proof", credentials=AnonymousCredentials())
# Optional workaround under test: client._bucket_metadata_cache = None
try:
list(client.list_blobs("some-bucket", prefix="x/", max_results=1))
except Exception:
pass # objects.list fails offline; irrelevant to the point
time.sleep(1.0) # let the ACO daemon thread run
print(f"google-cloud-storage {storage.__version__}: "
f"get_bucket invoked {len(calls)} time(s) by one list_blobs() call")
Actual output
get_bucket() caller:
File ".../google/cloud/storage/_bucket_metadata_cache.py", line 98, in _fetch_background
bucket = self._client.get_bucket(bucket_name, timeout=10.0)
google-cloud-storage 3.11.0: get_bucket invoked 1 time(s) by one list_blobs() call
With client._bucket_metadata_cache = None uncommented, the count is 0.
Root cause (call chain)
_helpers.create_trace_span_helper wraps every operation. For any span not in {Storage.Client.getBucket, Storage.Client.lookupBucket, Storage.Bucket.reload, Storage.Bucket.exists} (i.e. object ops like listBlobs), it calls client._bucket_metadata_cache.get_or_queue_fetch(bucket_name).
- On a cache miss,
get_or_queue_fetch spawns a daemon thread → _bucket_metadata_cache._fetch_background.
_fetch_background calls self._client.get_bucket(bucket_name, timeout=10.0) → REST storage.buckets.get.
- On
Forbidden, it caches a fallback and returns; the object op proceeds. Every fresh client re-probes on first touch of each bucket.
Expected behavior
Object-level operations should not require storage.buckets.get. This is a long-standing, explicitly-recognized contract for this library — cf. googleapis/python-storage #528 ("Redesign of API so it does not need storage.buckets.get access for every single operation") and #1225 ("Need to be able to download objects without storage.buckets.get permission same as gsutil"). gsutil/gcloud storage operate on objects without buckets.get, and object-only IAM roles deliberately omit it.
At minimum, ACO's background metadata probe should be opt-out via a supported mechanism, and ideally should not fire for principals/operations that don't need bucket metadata.
Requested change
A supported opt-out, e.g. one or more of:
- A
Client constructor option (e.g. enable_bucket_metadata_cache: bool = True), and/or
- An environment variable mirroring the existing
ENABLE_GCS_PYTHON_CLIENT_OTEL_TRACES pattern (e.g. DISABLE_GCS_PYTHON_CLIENT_BUCKET_METADATA_CACHE), and/or
- Making the probe lazy/suppressible so a
Forbidden result is cached once per client without re-emitting audit-log errors, and documenting that ACO requires storage.buckets.get.
Any of these lets callers using object-only roles avoid the audit-log noise without granting storage.buckets.get, and gives a stable alternative to mutating the private _bucket_metadata_cache attribute.
Happy to open a PR for option 1 or 2 if a maintainer confirms the preferred shape.
Environment details
google-cloud-storage3.11.0and3.12.0(theBucketMetadataCache/ ACO code path is present in both)Summary
Client.__init__unconditionally instantiates aBucketMetadataCache("App-centric Observability" / ACO). As a side effect, ordinary object-level operations (list_blobs,download_*,upload_*, etc.) now trigger a backgroundClient.get_bucket()call, i.e. the RESTstorage.buckets.getpermission — even though the application code never requests bucket metadata and the operation itself only needsstorage.objects.*.For principals granted object-only roles (
roles/storage.objectViewer,roles/storage.objectAdmin) — which intentionally excludestorage.buckets.get— this produces a continuous stream of deniedstorage.buckets.getentries in Cloud Audit Logs (data_access, severity ERROR), one per bucket per client instance.The failure is swallowed internally (data operations still succeed), so it is not a functional outage — but it is:
storage.buckets.get(e.g. viaroles/storage.bucketViewer/legacyBucketReader) purely to satisfy a telemetry probe the application does not use, across every bucket the service account ever touches. This directly conflicts with least-privilege.There is no supported way to disable it — no client option and no environment variable (the only observability env var,
ENABLE_GCS_PYTHON_CLIENT_OTEL_TRACES, is a separate opt-in for OpenTelemetry tracing). The only workaround is mutating the private attributeclient._bucket_metadata_cache = None, which is fragile and fails open on library upgrades.Steps to reproduce
Object-only operations cause a background
get_bucket()(→storage.buckets.get). The repro below is fully offline and needs no credentials or real bucket — it monkeypatchesget_bucketonly to observe that it is invoked and by whom:Actual output
With
client._bucket_metadata_cache = Noneuncommented, the count is0.Root cause (call chain)
_helpers.create_trace_span_helperwraps every operation. For any span not in{Storage.Client.getBucket, Storage.Client.lookupBucket, Storage.Bucket.reload, Storage.Bucket.exists}(i.e. object ops likelistBlobs), it callsclient._bucket_metadata_cache.get_or_queue_fetch(bucket_name).get_or_queue_fetchspawns a daemon thread →_bucket_metadata_cache._fetch_background._fetch_backgroundcallsself._client.get_bucket(bucket_name, timeout=10.0)→ RESTstorage.buckets.get.Forbidden, it caches a fallback and returns; the object op proceeds. Every fresh client re-probes on first touch of each bucket.Expected behavior
Object-level operations should not require
storage.buckets.get. This is a long-standing, explicitly-recognized contract for this library — cf. googleapis/python-storage #528 ("Redesign of API so it does not needstorage.buckets.getaccess for every single operation") and #1225 ("Need to be able to download objects withoutstorage.buckets.getpermission same as gsutil").gsutil/gcloud storageoperate on objects withoutbuckets.get, and object-only IAM roles deliberately omit it.At minimum, ACO's background metadata probe should be opt-out via a supported mechanism, and ideally should not fire for principals/operations that don't need bucket metadata.
Requested change
A supported opt-out, e.g. one or more of:
Clientconstructor option (e.g.enable_bucket_metadata_cache: bool = True), and/orENABLE_GCS_PYTHON_CLIENT_OTEL_TRACESpattern (e.g.DISABLE_GCS_PYTHON_CLIENT_BUCKET_METADATA_CACHE), and/orForbiddenresult is cached once per client without re-emitting audit-log errors, and documenting that ACO requiresstorage.buckets.get.Any of these lets callers using object-only roles avoid the audit-log noise without granting
storage.buckets.get, and gives a stable alternative to mutating the private_bucket_metadata_cacheattribute.Happy to open a PR for option 1 or 2 if a maintainer confirms the preferred shape.