Skip to main content

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 = true
  • public_access_prevention = "enforced" — unless the bucket has a documented public-read use case (merchant branding assets or public SDK distribution artifacts)
  • versioning.enabled = true
  • logging.log_bucket points to access_logs_sink, except for the dedicated sink bucket itself
  • No IAM member binding to allUsers or allAuthenticatedUsers unless the bucket has a documented public-read use case

Per-bucket State

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).
gateway-ios-sdk-<project>enforcedinherited (intentional, public SwiftPM/Xcode distribution)Explicit origin allowlist (developer.apple.com, swift.org)yes (roles/storage.objectViewer) — documented as public SDK artifact hostingPublic 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.co
  • https://pay.peakgateway.co
  • https://checkout.peakgateway.co
  • https://staging-portal.peakgateway.co
  • https://staging-pay.peakgateway.co
  • https://staging-checkout.peakgateway.co

ios_sdk_distribution allows:

  • https://developer.apple.com
  • https://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.