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 = truepublic_access_prevention = "enforced"— unless the bucket has a documented public-read use case (merchant branding assets)versioning.enabled = truelogging.log_bucketpoints toaccess_logs_sink- No IAM member binding to
allUsersorallAuthenticatedUsersunless the bucket has a documented public-read use case
Per-bucket state (verified 2026-04-17, SEC-03 / #402)
| Bucket | UBLA | Public Access Prevention | CORS | allUsers IAM? | Notes |
|---|---|---|---|---|---|
gateway-access-logs-sink-<project> | enforced | enforced | none | no | Destination 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> | enforced | enforced | none | no | Access logs for other buckets. No application IAM. |
gateway-settlement-reports-<project> | enforced | enforced | none | no | Settlement report exports. No application IAM (writer granted via workflow SA, not in Terraform). |
gateway-merchant-assets-<project> | enforced | inherited (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 hosting | Management 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.cohttps://pay.peakgateway.cohttps://checkout.peakgateway.cohttps://staging-portal.peakgateway.cohttps://staging-pay.peakgateway.cohttps://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_assetsCORS origin was wildcard (*) and replaced it with an explicit allowlist invar.merchant_assets_cors_originswith avalidationblock that rejects*going forward.