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 holdsroles/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 noserviceAccountis exactly such a workload: it 403s onstorage.objects.getreading back its own uploaded source. Build identity is therefore pinned explicitly in all fourcloudbuild*.yamlconfigs, and thecloud-build-runnerSA plus its project bindings are now declared ininfrastructure-gcp/terraform/environments/production/main.tfrather than existing out of band. That SA’s project-widesecretmanager.secretAccessorandiam.serviceAccountUserhave since been narrowed (see the update below), leaving 7 project roles.
Update (2026-08-10, later):
cloud-build-runner’s project-wideroles/secretmanager.secretAccessorandroles/iam.serviceAccountUserare 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) oractAsany 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:secretAccessoron exactly the 12 secrets incloudbuild.yaml’savailableSecrets, andserviceAccountUseron the 9 runtime SAs the deploy steps actuallyactAs(5 Cloud Run + 4 VM). Note for future changes:gcloud run deployneedsactAson the revision’s SA even when--service-accountis omitted, and OS Login needsactAson the target instance’s own SA — the latter surfaces as a barePermission denied (publickey)rather than a 403. Verified by a forced full deploy on both staging and main. Still open:github-deployerholds project-wideiam.serviceAccountUserpluscloudbuild.builds.editorand authenticates from a static JSON key in theGCP_SA_KEYGitHub secret — the same escalation shape, one hop earlier.
Findings
Section titled “Findings”| 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 | 12615784853-compute@developer holds roles/editor |
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. |
Top 3 to fix first
Section titled “Top 3 to fix first”- Enable Data Access audit logs (High) — currently no record of secret/data reads.
- Strip
roles/editorfrom the default compute SA (High) — least-privilege for the SA that runs workloads. - Put Cloud Armor + an HTTPS LB in front of the public prod webapp (High) — there is no WAF on the real edge today.
Gated / manual checks
Section titled “Gated / manual checks”- No checks were classifier-denied this run (SA key listing succeeded).
- Confirm the
github-deployerkey dated 2026-06-25 is the deliberate rotation (it is — seesecret-rotation-playbook) and the prior key was deleted, not orphaned (confirmed deleted). - Confirm
terraform-infra(Mar) anddratareadonly(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-stagingbefore tightening ingress tointernal.