From e8d1d8607ec5116d15154e7063eeedc6846e7081 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:38:59 +0200 Subject: [PATCH 1/3] docs(guide): record the bucket public-access policy (no public listing) allUsers on anyplot-images swapped from objectViewer (bundles storage.objects.list - the whole bucket incl. staging/ was publicly enumerable) to legacyObjectReader (get-only), applied 2026-08-19 via gcloud. Bucket IAM does not deploy with the repo, so the runbook carries the policy and the re-apply command, next to the CORS section. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 +++++++ agentic/docs/project-guide.md | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b61494c7b..fc56059842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,13 @@ aggregate instead: an italic *Catalog* line at the end of the version section an were served as bare `text/plain`, which a strict client decodes as Latin-1 mojibake. nginx now declares `charset utf-8` on text responses in both server blocks, and the daily bot-serving monitor asserts it on `llms.txt` (#10492). +- **The image bucket is no longer publicly listable** — `allUsers` held + `roles/storage.objectViewer` on `anyplot-images`, whose bundled `storage.objects.list` let + anyone enumerate the whole bucket, including the `staging/` prefix where unreviewed pre-merge + renders sit indistinguishable from promoted ones. Swapped to `roles/storage.legacyObjectReader` + (get-only): every render URL keeps working (verified live incl. CORS), only the directory + listing is gone. Applied 2026-08-19 via gcloud — bucket IAM does not deploy with the repo; the + policy and re-apply procedure are recorded in the project guide (#10492). ### Changed diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index 3f640d5196..716a108f49 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -395,6 +395,26 @@ page under `COEP: require-corp` must load the renders with a `crossorigin` attribute (or through its own proxy); plain same-tab fetches and `` tags are unaffected. +### Bucket public-access policy + +Objects are publicly fetchable by URL, but the bucket is NOT publicly listable: +`allUsers` holds `roles/storage.legacyObjectReader` (grants `storage.objects.get` +only). It previously held `roles/storage.objectViewer`, whose bundled +`storage.objects.list` let anyone enumerate the whole bucket — including the +`staging/` prefix, where unreviewed pre-merge renders sit indistinguishable +from promoted ones (AI-access audit 2026-08-19; changed 2026-08-19). Like the +CORS policy above, this is bucket metadata that does not deploy with the repo — +re-apply after a bucket rebuild: + +```bash +gcloud storage buckets add-iam-policy-binding gs://anyplot-images \ + --member=allUsers --role=roles/storage.legacyObjectReader +``` + +Never grant `allUsers` a role that includes `storage.objects.list`. To verify: +an object URL must return 200 anonymously, while +`https://storage.googleapis.com/storage/v1/b/anyplot-images/o` must return 401. + ## Tech Stack - **Backend**: FastAPI, SQLAlchemy (async), PostgreSQL, Python 3.13+ From 158f811eee9364a53a264d30d3e019be38bee290 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:40:34 +0200 Subject: [PATCH 2/3] docs(changelog): the bucket-policy entry ships in #10493 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc56059842..ad6265ee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,7 +143,7 @@ aggregate instead: an italic *Catalog* line at the end of the version section an renders sit indistinguishable from promoted ones. Swapped to `roles/storage.legacyObjectReader` (get-only): every render URL keeps working (verified live incl. CORS), only the directory listing is gone. Applied 2026-08-19 via gcloud — bucket IAM does not deploy with the repo; the - policy and re-apply procedure are recorded in the project guide (#10492). + policy and re-apply procedure are recorded in the project guide (#10493). ### Changed From 43852d67eb6ca28f7facb3f9ecd5860ee63a0c38 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:45:08 +0200 Subject: [PATCH 3/3] docs(guide): make the bucket-policy runbook idempotent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on #10493: the re-apply snippet only added the get-only role — if an objectViewer binding regains a foothold, that alone does not remove public listability. Both commands documented. Co-Authored-By: Claude Fable 5 --- agentic/docs/project-guide.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index 716a108f49..495ad3a201 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -404,11 +404,15 @@ only). It previously held `roles/storage.objectViewer`, whose bundled `staging/` prefix, where unreviewed pre-merge renders sit indistinguishable from promoted ones (AI-access audit 2026-08-19; changed 2026-08-19). Like the CORS policy above, this is bucket metadata that does not deploy with the repo — -re-apply after a bucket rebuild: +re-apply after a bucket rebuild. Both commands are needed: adding the get-only +role does not remove public listability if an `objectViewer` binding exists +(the removal is a no-op when the binding is already absent): ```bash gcloud storage buckets add-iam-policy-binding gs://anyplot-images \ --member=allUsers --role=roles/storage.legacyObjectReader +gcloud storage buckets remove-iam-policy-binding gs://anyplot-images \ + --member=allUsers --role=roles/storage.objectViewer ``` Never grant `allUsers` a role that includes `storage.objects.list`. To verify: