Skip to main content

Cloud Storage Bucket IAM Audit

Checklist for verifying every google_storage_bucket in infra/terraform/modules/storage/ meets the security baseline. Re-run any time a bucket is added or its IAM changes.

Baseline (every bucket)

  • uniform_bucket_level_access = true
  • public_access_prevention = "enforced" — unless the bucket has a documented public-read use case (merchant branding assets)
  • versioning.enabled = true
  • logging.log_bucket points to access_logs_sink
  • No IAM member binding to allUsers or allAuthenticatedUsers unless the bucket has a documented public-read use case

Per-bucket state (verified 2026-04-17, SEC-03 / #402)

BucketUBLAPublic Access PreventionCORSallUsers IAM?Notes
gateway-access-logs-sink-<project>enforcedenforcednonenoDestination sink for access logs; IAM member group:cloud-storage-analytics@google.com with roles/storage.objectCreator is the standard GCS log writer principal.
gateway-access-logs-<project>enforcedenforcednonenoAccess logs for other buckets. No application IAM.
gateway-settlement-reports-<project>enforcedenforcednonenoSettlement report exports. No application IAM (writer granted via workflow SA, not in Terraform).
gateway-merchant-assets-<project>enforcedinherited (intentional, merchant-branding logos are public-read)Explicit origin allowlist (portal, pay, checkout on prod + staging)yes (roles/storage.objectViewer) — documented as merchant logo/branding hostingManagement service SA has roles/storage.objectCreator (upload).

IAM principals granted per bucket

gateway-access-logs-sink-<project>:
- group:cloud-storage-analytics@google.com roles/storage.objectCreator
(GCS system group that writes access logs — NOT a real user group)

gateway-access-logs-<project>:
(none explicit)

gateway-settlement-reports-<project>:
(none explicit)

gateway-merchant-assets-<project>:
- allUsers roles/storage.objectViewer
(intentional — public-read merchant logos)
- serviceAccount:gateway-management@... roles/storage.objectCreator
(upload path for merchant logo uploads from admin portal)

CORS policy

Only merchant_assets has CORS configured. As of SEC-03, the origin list is an explicit allowlist declared in infra/terraform/modules/storage/variables.tf under merchant_assets_cors_origins. Wildcard * is rejected by the variable's validation block, so it cannot be reintroduced without an intentional code change.

Current allowlist (prod + staging):

  • https://portal.peakgateway.co
  • https://pay.peakgateway.co
  • https://checkout.peakgateway.co
  • https://staging-portal.peakgateway.co
  • https://staging-pay.peakgateway.co
  • https://staging-checkout.peakgateway.co

Verification commands

# Terraform inventory
grep -R 'resource "google_storage_bucket"' infra/terraform/modules/storage/

# Per-bucket live IAM — prod
for bucket in \
"gateway-access-logs-sink-${GCP_PROJECT}" \
"gateway-access-logs-${GCP_PROJECT}" \
"gateway-settlement-reports-${GCP_PROJECT}" \
"gateway-merchant-assets-${GCP_PROJECT}"
do
echo "=== ${bucket} ==="
gcloud storage buckets get-iam-policy "gs://${bucket}" \
--project "${GCP_PROJECT}" --format=json \
| jq '.bindings[] | select(.members[] | test("allUsers|allAuthenticatedUsers"))'
done

The jq filter returns an empty result for every bucket except gateway-merchant-assets-<project>, where the documented allUsers / roles/storage.objectViewer binding is expected.

History

  • 2026-04-17 — SEC-03 audit (#402): verified UBLA and public-access prevention on every bucket, enumerated IAM principals, found the merchant_assets CORS origin was wildcard (*) and replaced it with an explicit allowlist in var.merchant_assets_cors_origins with a validation block that rejects * going forward.