Google Cloud IAM controls who can do what to which resource across every project in your organization, and it fails quietly. There's no exploit chain, no CVE, no alert — just a service account with roles/editor instead of roles/pubsub.publisher, sitting unused for 400 days until an attacker finds it. Google Cloud Build's default service account carried project-wide editor permissions until an August 2024 disclosure (Tenable's "Bad.Build") showed how it let attackers escalate to control Artifact Registry and other services. A separate 2023 flaw nicknamed "GhostToken," reported by Astrix Security and patched by Google that June, let attackers hide malicious OAuth grants from a victim's own GCP account page. Neither required zero-days. Both required IAM to be misconfigured. This post covers the specific controls — role scoping, service account hardening, Conditions, org policies, and continuous review — that close those gaps.
Why is least privilege the foundation of GCP IAM security?
Least privilege matters because GCP's basic roles grant far more access than almost any identity needs, and that gap is what attackers exploit after an initial foothold. The roles/owner basic role bundles nearly every permission across every enabled API in a project, including the ability to modify IAM policy itself; roles/editor grants create/modify/delete on almost all resources but not IAM changes. Google has published more than 700 predefined roles specifically so teams don't have to fall back to Owner or Editor, yet basic roles remain the default many engineers reach for when a gcloud command throws a permission-denied error under deadline pressure. Industry-wide cloud threat research has repeatedly found that the large majority of granted cloud permissions go unused — in some analyses, over 90% — meaning a compromised credential typically hands an attacker far more reach than the workload it belongs to ever needed. The fix is mechanical: replace Owner/Editor grants with predefined roles (roles/storage.objectViewer instead of roles/editor for a service that only reads from a bucket), and use the IAM Recommender, which is on by default at the project level, to flag roles where 90+ days of Policy Analyzer logs show no use of granted permissions.
What's the real difference between GCP's basic, predefined, and custom roles?
The real difference is blast radius: basic roles apply org-wide across services, predefined roles scope to one service's typical tasks, and custom roles let you scope to the exact permission list a workload uses. A predefined role like roles/bigquery.dataViewer grants bigquery.datasets.get, bigquery.tables.getData, and a handful of related read permissions — nothing else. Custom roles go further: you compose a role from an exact permission set (Google caps this at 300 custom roles per project and 300 per organization) and version it like code. The tradeoff is maintenance — custom roles don't automatically pick up new permissions when Google ships product updates, so a custom role built in 2022 for Cloud Run may be missing permissions for features GA'd in 2024, silently breaking a deployment pipeline instead of over-granting access. A workable default: use predefined roles for 80-90% of grants, reserve custom roles for the small number of service accounts running high-privilege automation (CI/CD deploy accounts, cross-project data pipelines) where the extra maintenance is worth the tighter scope.
How do service accounts become the biggest IAM risk in GCP?
Service accounts become the biggest IAM risk because they're non-human, long-lived, and routinely over-permissioned by default. Every GCP project auto-creates a Compute Engine default service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com); prior to Google's policy change in May 2024, that account was automatically granted roles/editor on the project unless an org policy blocked it. Any VM or GKE node running under that default identity — and plenty still do in projects created before the change — inherits near-full write access to the project without anyone explicitly granting it. Downloadable JSON service account keys compound the problem: a key checked into a public GitHub repo, a CI log, or a laptop backup is a permanent credential with no MFA and no session expiry until someone manually rotates it. Google's own guidance is to disable service account key creation org-wide via the iam.disableServiceAccountKeyCreation constraint and use Workload Identity Federation or attached service accounts instead, which issue short-lived tokens tied to a workload's identity rather than a static secret. Where keys can't be eliminated (some third-party integrations still require them), enforce 90-day rotation and alert on any key used from an IP range or region the workload has never operated in.
How do IAM Conditions and org policies shrink the attack surface beyond role scoping?
IAM Conditions and org policies shrink the attack surface by adding context — time, resource attributes, request origin — on top of role grants, so a role that's correct in general can still be denied in a specific situation. A Condition can restrict a roles/storage.objectAdmin grant to only apply to resources matching resource.name.startsWith("projects/_/buckets/prod-backups"), or expire a temporary contractor's access automatically with request.time < timestamp("2026-09-01T00:00:00Z") instead of relying on someone remembering to revoke it. Organization Policy Constraints operate a level up, blocking classes of misconfiguration org-wide regardless of what any individual IAM binding says: iam.disableServiceAccountKeyCreation stops new JSON keys from being minted, iam.allowedPolicyMemberDomains blocks external Gmail accounts from ever being added as principals, and constraints/iam.automaticIamGrantsForDefaultServiceAccounts (the constraint Google flipped to enforced-by-default for new projects in 2024) stops the auto-Editor grant described above. The combination matters because role bindings answer "what can this identity do," while Conditions and org policies answer "under what circumstances" and "never, no matter who asks" — both are needed to actually bound risk.
How do you catch IAM drift before it reaches production instead of during an annual audit?
You catch IAM drift before production by treating IAM bindings as code reviewed in CI, not as console clicks reviewed once a year. GCP's Policy Analyzer (part of the Policy Intelligence suite) lets you query, at any point in time, which principals have a given permission on a given resource and why — the answer is often "an inherited org-level binding from 14 months ago" rather than anything intentional. Policy Simulator lets you test a proposed IAM change against real historical access logs before applying it, catching the case where tightening a role would have broken a legitimate job that ran last Tuesday. The durable fix is to manage IAM bindings through Terraform or Config Connector with mandatory pull request review, run gcloud asset analyze-iam-policy or an equivalent policy-as-code check in CI to block PRs that add roles/owner or roles/editor outside an explicit exception list, and pipe Cloud Audit Logs' SetIamPolicy events to your SIEM so any binding change made outside that pipeline — through the console, gcloud, or the API directly — generates an alert within minutes rather than surfacing at the next quarterly review.
How Safeguard Helps
Safeguard extends this IAM discipline into the software supply chain that runs on top of those GCP permissions. Griffin AI, Safeguard's triage engine, correlates IAM misconfigurations — like a build service account with unused storage.admin scope — against the actual code paths reachable at runtime, so security teams aren't chasing every over-permissioned identity in isolation but prioritizing the ones a real vulnerability could reach. Reachability analysis confirms whether a vulnerable dependency sitting behind an over-privileged service account is actually exercised in the call graph, cutting the alert volume that typically buries GCP IAM Recommender findings. Safeguard generates and ingests SBOMs across your GCP-hosted services to map which workloads use which service accounts and dependencies together, and where a fix is available, it opens an auto-fix PR — whether that's a dependency bump or a Terraform diff tightening an IAM binding — so remediation lands as a reviewed code change instead of a manual console edit no one tracks.