Surfaced by the security-scan fix in #165. These are reported to the Security tab but do not block deploys (the gate is HIGH/CRITICAL only), so they need a deliberate decision.
Findings
| Severity |
Location |
Rule |
| MEDIUM |
main.tf:41 |
GCP-0078 — storage bucket public access |
| MEDIUM |
main.tf:41 |
GCP-0077 — bucket not encrypted with customer-managed key |
| MEDIUM |
main.tf:123 |
GCP-0077 |
| MEDIUM |
iam.tf:271 |
GCP-0011 |
| LOW |
main.tf:41 |
GCP-0066 — bucket logging not enabled |
| LOW |
main.tf:123 |
GCP-0066 |
| LOW |
monitoring.tf:129 |
GCP-0079 |
The one that matters: main.tf:41 — a public bucket nobody uses
google_storage_bucket.media_storage grants allUsers → roles/storage.objectViewer (main.tf:62-70). It is publicly readable.
It is also dead. Media storage migrated to Cloudflare R2 (R2StorageService.kt, R2AdminService.kt); a grep for gcs|google.cloud.storage|storage.googleapis across apps/api/src returns zero hits, and there is no GCS dependency in build.gradle.kts. CLAUDE.md also states GCP media storage is deferred.
So this is a publicly-readable bucket that no code reads from or writes to.
Careful — the dead variable is load-bearing
Removing it is not a one-liner. GCS_BUCKET_NAME is:
- injected into the backend container (
cloudrun.tf:81-84)
- listed in
required_secrets in backend-ci.yml:377, where the loop at :379-387 exit 1s the production deploy if it is missing from the Cloud Run env
So deleting the bucket + env var without also updating backend-ci.yml:377 will break backend deploys.
Also worth noting: required_secrets checks GCS_BUCKET_NAME but does not check any of the R2 / AI / YouTube vars the app actually uses — it is verifying the wrong list.
Suggested change (atomic)
- Remove
google_storage_bucket.media_storage, the allUsers IAM binding, the objectAdmin grants (iam.tf:215-219, cicd_media_storage_access), the GCS_BUCKET_NAME env var, and the two outputs (outputs.tf:3-6, 93-96)
- Remove
GCS_BUCKET_NAME from required_secrets in backend-ci.yml:377 in the same commit
- Consider adding the R2 vars to
required_secrets instead
- Keep
google_project_service.storage (apis.tf:47) — the Terraform state bucket needs it
The remaining LOW/MEDIUM findings (CMEK, bucket logging) are separate hardening decisions.
Surfaced by the security-scan fix in #165. These are reported to the Security tab but do not block deploys (the gate is HIGH/CRITICAL only), so they need a deliberate decision.
Findings
main.tf:41main.tf:41main.tf:123iam.tf:271main.tf:41main.tf:123monitoring.tf:129The one that matters:
main.tf:41— a public bucket nobody usesgoogle_storage_bucket.media_storagegrantsallUsers→roles/storage.objectViewer(main.tf:62-70). It is publicly readable.It is also dead. Media storage migrated to Cloudflare R2 (
R2StorageService.kt,R2AdminService.kt); a grep forgcs|google.cloud.storage|storage.googleapisacrossapps/api/srcreturns zero hits, and there is no GCS dependency inbuild.gradle.kts.CLAUDE.mdalso states GCP media storage is deferred.So this is a publicly-readable bucket that no code reads from or writes to.
Careful — the dead variable is load-bearing
Removing it is not a one-liner.
GCS_BUCKET_NAMEis:cloudrun.tf:81-84)required_secretsinbackend-ci.yml:377, where the loop at:379-387exit 1s the production deploy if it is missing from the Cloud Run envSo deleting the bucket + env var without also updating
backend-ci.yml:377will break backend deploys.Also worth noting:
required_secretschecksGCS_BUCKET_NAMEbut does not check any of the R2 / AI / YouTube vars the app actually uses — it is verifying the wrong list.Suggested change (atomic)
google_storage_bucket.media_storage, theallUsersIAM binding, theobjectAdmingrants (iam.tf:215-219, cicd_media_storage_access), theGCS_BUCKET_NAMEenv var, and the two outputs (outputs.tf:3-6, 93-96)GCS_BUCKET_NAMEfromrequired_secretsinbackend-ci.yml:377in the same commitrequired_secretsinsteadgoogle_project_service.storage(apis.tf:47) — the Terraform state bucket needs itThe remaining LOW/MEDIUM findings (CMEK, bucket logging) are separate hardening decisions.