Skip to content

GCP Cloud-Config Security Audit — ordermatic-prod

Date: 2026-06-25 · Scope: project ordermatic-prod (12615784853), read-only · Account: david@ordermatic.co Companion to: compliance-roadmap.md (ADA Cloud profile) · Status: DRAFT findings

All checks are read-only (describe/list/get-iam-policy). The app’s real edge is confirmed as raw Cloud Run *.run.app (no HTTPS LB / Cloud Armor in front) — relevant because app.ordermatic.co is not Cloudflare-proxied, so cloud-side edge controls are GCP’s responsibility.

Update (2026-06-25): the Cloud Run WAF/edge High finding was remediated by #1926 (Cloud Armor + HTTPS LB landed). Data Access audit logs (High) handed off as a ready-to-run command.

Update (2026-08-10): the default compute SA (12615784853-compute@developer) no longer holds roles/editor — or any project-level binding. This closes the “primitive role on default SA” High finding, but it also means every workload that implicitly runs as that SA now fails closed. A Cloud Build build that names no serviceAccount is exactly such a workload: it 403s on storage.objects.get reading back its own uploaded source. Build identity is therefore pinned explicitly in all four cloudbuild*.yaml configs, and the cloud-build-runner SA plus its project bindings are now declared in infrastructure-gcp/terraform/environments/production/main.tf rather than existing out of band. That SA’s project-wide secretmanager.secretAccessor and iam.serviceAccountUser have since been narrowed (see the update below), leaving 7 project roles.

Update (2026-08-10, later): cloud-build-runner’s project-wide roles/secretmanager.secretAccessor and roles/iam.serviceAccountUser are removed, closing the escalation path where anyone able to submit a build as this SA could read every prod secret (including the Infisical bootstrap credentials) or actAs any SA in the project — including deploying a Cloud Run revision or VM startup-script as a higher-privileged SA and taking its token. Replaced with per-resource grants: secretAccessor on exactly the 12 secrets in cloudbuild.yaml’s availableSecrets, and serviceAccountUser on the 9 runtime SAs the deploy steps actually actAs (5 Cloud Run + 4 VM). Note for future changes: gcloud run deploy needs actAs on the revision’s SA even when --service-account is omitted, and OS Login needs actAs on the target instance’s own SA — the latter surfaces as a bare Permission denied (publickey) rather than a 403. Verified by a forced full deploy on both staging and main. Still open: github-deployer holds project-wide iam.serviceAccountUser plus cloudbuild.builds.editor and authenticates from a static JSON key in the GCP_SA_KEY GitHub secret — the same escalation shape, one hop earlier.

Control Finding Severity Fix
Cloud Run WAF / edge App edge is raw *.run.app — zero Cloud Armor policies, no HTTPS LB / backend services / URL maps. erp-webapp-prod + -staging are ingress=all + allUsers invoker (public by design) but have no WAF, rate-limiting, or L7 filtering. High → ✅ Remediated by #1926 (Cloud Armor + HTTPS LB landed). Verify Cloud Run ingress is now internal-and-cloud-load-balancing.
Data Access audit logs Project IAM auditConfigs is null — Data Access audit logging off. Only always-on Admin Activity logs exist. No record of who read secrets/storage/data. High Enable Data Access audit logs (DATA_READ/DATA_WRITE) project-wide, at least for IAM, Secret Manager, Storage, Cloud SQL.
Primitive role on default SA Default compute SA 12615784853-compute@developer holds roles/editor — no longer true as of 2026-08. That SA now holds zero project-level bindings (Google stopped auto-granting project-editor). Blast radius is gone; the failure mode is now the opposite — anything that falls back to it 403s. See the update note below. High → ✅ No action. Do not re-grant. Build identity is pinned explicitly instead (cloud-build-runner@, codified in the production Terraform stack).
Cloud Run ingress vs auth erp-email-worker-prod + erp-internal-staging are ingress=all with no invoker binding (not publicly invokable; IAM-only). Ingress should match intent. Medium Set ingress to internal for the worker + internal services (defense in depth).
User-managed SA keys Long-lived JSON keys on github-deployer (created 2026-06-25 — the deliberate rotation), terraform-infra (2026-03-14), dratareadonly (2026-05-23). Medium Prefer Workload Identity Federation; if keys required, enforce key-expiry org policy + rotation. Verify terraform-infra / dratareadonly are within policy.
GCS uniform bucket-level access ordermatic-prod_cloudbuild has UBLA=False (legacy ACLs) and public_access_prevention=inherited (not enforced). Medium Enable UBLA + public_access_prevention=enforced.
roles/owner Held by one human (david@ordermatic.co) — acceptable. roles/editor also on terraform/cloudservices SAs (expected). Low Acceptable; scope Terraform’s roles long-term.
Firewall: 0.0.0.0/0 on SSH/RDP/ICMP default-allow-ssh/rdp/icmp open to 0.0.0.0/0 — but on the unused default network (zero instances). Real VMs are on erp-prod-vpc/erp-staging-vpc, no external IPs, SSH via IAP 35.235.240.0/20. Currently unreachable. Low Delete the unused default network + its rules to remove latent exposure.
GCS public exposure Only bucket ordermatic-prod_cloudbuild; IAM = projectOwner/Editor/Viewer only — not public. Low None for exposure (see UBLA above).
Reserved public IPs Two external IPs are both Cloud NAT egress (egress-only). No stray inbound IPs. Low None.
Encryption (note) Buckets use Google-managed keys (no CMEK observed). Acceptable baseline. Info CMEK only if a compliance regime mandates it.
Secrets posture (note) App runtime secrets live in Infisical (not env/GCS); secretAccessor scoped to build/deploy/compute SAs. Reasonable. Info None.
  1. Enable Data Access audit logs (High) — currently no record of secret/data reads.
  2. Strip roles/editor from the default compute SA (High) — least-privilege for the SA that runs workloads.
  3. Put Cloud Armor + an HTTPS LB in front of the public prod webapp (High) — there is no WAF on the real edge today.
  • No checks were classifier-denied this run (SA key listing succeeded).
  • Confirm the github-deployer key dated 2026-06-25 is the deliberate rotation (it is — see secret-rotation-playbook) and the prior key was deleted, not orphaned (confirmed deleted).
  • Confirm terraform-infra (Mar) and dratareadonly (May) keys are within an intended rotation window — no org-policy key-expiry enforces this automatically.
  • Cross-check that nothing external legitimately calls erp-email-worker-prod / erp-internal-staging before tightening ingress to internal.