Cloud Storage Bucket IAM Audit
Checklist for verifying every google_storage_bucket in
infra/tf/gcp/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 or public SDK distribution artifacts)versioning.enabled = truelogging.log_bucketpoints toaccess_logs_sink, except for the dedicated sink bucket itself- No IAM member binding to
allUsersorallAuthenticatedUsersunless the bucket has a documented public-read use case
Per-bucket State
| 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). |
gateway-ios-sdk-<project> | enforced | inherited (intentional, public SwiftPM/Xcode distribution) | Explicit origin allowlist (developer.apple.com, swift.org) | yes (roles/storage.objectViewer) — documented as public SDK artifact hosting | Public read is limited to SDK XCFramework zip distribution. |
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)
gateway-ios-sdk-<project>:
- allUsers roles/storage.objectViewer
(intentional — public-read SDK release artifacts)
CORS policy
Only the public-read buckets have CORS configured.
merchant_assets uses an explicit origin allowlist declared in
infra/tf/gcp/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
ios_sdk_distribution allows:
https://developer.apple.comhttps://swift.org
Verification commands
# Terraform inventory
grep -R 'resource "google_storage_bucket"' infra/tf/gcp/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}" \
"gateway-ios-sdk-${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> and gateway-ios-sdk-<project>, where
the documented allUsers / roles/storage.objectViewer bindings are expected.