Google Cloud IAM Explained: Roles, Policies, and the Resource Hierarchy
Nearly every serious security incident in a cloud environment traces back to a permissions problem โ not a zero-day exploit, but a role granted too broadly, a service account key that should have expired, or a policy inherited from a level of the resource hierarchy nobody remembered existed. Cloud Identity and Access Management (IAM) is the system that determines who can do what to which resource in GCP, and understanding it deeply โ not just โgrant the Editor role and move onโ โ is one of the highest-leverage security skills you can build as a GCP practitioner.
This isnโt a features list. Itโs the mental model IAM actually runs on: how policies attach, how they inherit, and where the gap between โtechnically correctโ and โactually secureโ tends to open up.
The Resource Hierarchy
Every IAM policy binds to a node in GCPโs resource hierarchy, and permissions inherit downward โ this single fact explains most IAM behavior that otherwise seems confusing.
Organization โ โโโ Folder (e.g., "Engineering") โ โ โ โโโ Project A โ โ โโโ Resources (buckets, VMs, datasets) โ โ โ โโโ Project B โ โโโ Resources โ โโโ Folder (e.g., "Finance") โโโ Project CA role granted at the Organization level applies to every folder, project, and resource beneath it. A role granted at a Folder level applies to every project in that folder. This inheritance is additive only โ you cannot deny a permission at a lower level that was granted higher up (thatโs what IAM Deny policies exist to solve, covered below). This is the single most common source of โwhy does this service account have access I never explicitly granted itโ โ the answer is almost always an inherited binding from a folder or organization level nobody checked.
Three Types of Roles
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ Role type โ Description โ Example โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโคโ Basic โ Broad, legacy (Owner/Editor/ โ roles/editor โโ โ Viewer) โ avoid in production โ โโ Predefined โ Google-curated, service-scoped โ roles/storage.objectViewerโโ Custom โ You define the exact permission โ myCompany.bqReadOnly โโ โ set โ โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโBasic roles (roles/owner, roles/editor, roles/viewer) predate the modern IAM system and grant sweeping access across every service in a project โ Editor alone includes permission to modify nearly everything. Theyโre still around for legacy compatibility, but granting roles/editor to a service account is almost always a sign the access model hasnโt been thought through, because it grants far more than any specific workload actually needs.
Predefined roles are Googleโs curated, service-scoped bundles โ roles/storage.objectViewer, roles/bigquery.dataEditor, roles/compute.instanceAdmin.v1. These are the right default for most situations because theyโre maintained by Google as services evolve, and they map to real job functions rather than โeverything.โ
Custom roles let you assemble an exact permission set when even the narrowest predefined role is still broader than what a workload needs. Custom roles are more precise but come with a maintenance cost โ they donโt automatically pick up new permissions when Google adds capability to a service, so they need periodic review.
Policy Bindings in Practice
An IAM policy is a set of bindings, each pairing a role with the principals (users, groups, service accounts) that hold it:
# Grant a service account read access to a specific BigQuery datasetgcloud projects add-iam-policy-binding my-project \ --member="serviceAccount:pipeline-sa@my-project.iam.gserviceaccount.com" \ --role="roles/bigquery.dataViewer" \ --condition=None
# View the current policy for a projectgcloud projects get-iam-policy my-project --format=json{ "bindings": [ { "role": "roles/bigquery.dataViewer", "members": [ "serviceAccount:pipeline-sa@my-project.iam.gserviceaccount.com" ] }, { "role": "roles/owner", "members": ["user:admin@company.com"] } ]}Reviewing this JSON directly (rather than only clicking through the console) is a habit worth building โ itโs the fastest way to spot an over-broad binding, like a basic role granted to an entire Google Group instead of a specific principal.
IAM Conditions: Scoping Access Further
IAM Conditions add fine-grained constraints to a binding โ time-based expiry, resource-name pattern matching, or request attributes โ without needing a fully custom role:
gcloud projects add-iam-policy-binding my-project \ --member="user:contractor@external.com" \ --role="roles/storage.objectViewer" \ --condition="expression=request.time < timestamp('2026-06-01T00:00:00Z'),title=temp-contractor-access"This grants access that automatically expires โ genuinely useful for contractor access, incident-response break-glass grants, or any situation where โtemporaryโ access has a habit of quietly becoming permanent because nobody remembered to revoke it manually.
Deny Policies: The Exception to โInheritance Only Grantsโ
IAM Deny policies, layered on top of the standard grant model, let you explicitly block specific permissions regardless of whatโs granted elsewhere in the hierarchy โ the one mechanism that can override an inherited grant rather than only adding to it.
gcloud iam deny-policies create block-external-sharing \ --organization=123456789 \ --rules-file=deny-rule.yamlrules: - denyRule: deniedPrincipals: - "principalSet://goog/public:all" deniedPermissions: - "storage.googleapis.com/storage.setIamPolicy" denialCondition: expression: "true"A rule like this can enforce โnobody, at any level of the hierarchy, can make a storage bucket publicly readableโ as an organization-wide guardrail โ a genuinely different capability from role-based grants, because it canโt be overridden by a well-meaning but under-informed engineer granting Storage Admin on a single project.
How a Request Is Actually Evaluated
When a principal calls an API, GCP evaluates the request against every applicable policy across the resource hierarchy before deciding allow or deny:
Deny policies are evaluated first and unconditionally win โ this ordering is why theyโre the right tool for organization-wide guardrails rather than trying to achieve the same effect by carefully never granting a permission anywhere (which is fragile, because it only takes one future binding to break the guarantee).
Auditing Effective Access
Because permissions accumulate across the hierarchy, the policy attached to a single resource doesnโt tell you the full story. The Policy Analyzer API answers the more useful question โ โwhat can this principal actually do, considering every inherited bindingโ:
gcloud asset analyze-iam-policy \ --organization=123456789 \ --identity="serviceAccount:pipeline-sa@my-project.iam.gserviceaccount.com" \ --full-resource-name="//cloudresourcemanager.googleapis.com/projects/my-project"Running this against every production service account periodically โ not just when something breaks โ is how mature teams catch privilege creep before it becomes the root cause of an incident report. Itโs a five-minute check that regularly surfaces a stale binding nobody remembers granting.
Real-World Use Case: Least-Privilege Pipeline Access
A data pipeline service account is a good illustration of getting this right. Instead of granting it roles/editor on the project (a common shortcut that โjust worksโ and is exactly the kind of decision that gets flagged in a security audit), a properly scoped setup grants:
roles/bigquery.dataEditoron the specific dataset it writes to, not project-wideroles/storage.objectVieweron the specific source bucket it reads fromroles/pubsub.subscriberon the specific subscription it consumes
Each grant is scoped to the resource, not the project, and none of them include permissions the pipeline doesnโt actually use (deleting datasets, managing IAM policies, provisioning new infrastructure). If that service accountโs key is ever compromised, the blast radius is three specific resources instead of the entire project.
Common Mistakes
Granting project-level roles when resource-level would do. roles/bigquery.dataEditor on an entire project instead of a specific dataset is a common shortcut that expands blast radius far beyond whatโs needed.
Using user accounts for service-to-service authentication. Long-lived user credentials embedded in application code are a recurring source of leaked-credential incidents โ service accounts, ideally with short-lived tokens via Workload Identity Federation, are the correct pattern.
Never auditing inherited permissions. A binding at the organization or folder level is easy to forget exists. Regularly reviewing effective permissions (not just the policy at the resource youโre looking at) catches accumulated over-privilege before it becomes an incident.
Treating custom roles as โset and forget.โ Custom roles donโt automatically gain new permissions as GCP services evolve โ a custom role built two years ago may be missing permissions for features added since, causing confusing failures that look like a bug rather than a stale role definition.
Best Practices
- Default to predefined roles scoped to the narrowest resource level that satisfies the actual need โ project-level only when the workload genuinely spans every resource in the project.
- Use groups, not individual users, in policy bindings โ managing access through group membership scales far better than editing IAM policies every time someone joins or leaves a team.
- Set up Policy Analyzer or IAM Recommender to surface unused permissions โ GCPโs own tooling flags roles granted but never actually exercised, which is a fast way to find over-privilege without manual audit.
- Use Conditions for anything genuinely temporary rather than relying on a calendar reminder to manually revoke access later.
Frequently Asked Questions
Whatโs the difference between IAM and ACLs on a resource like Cloud Storage? IAM is the unified, hierarchy-aware access control system spanning all of GCP; ACLs are an older, resource-specific mechanism (still used on individual Cloud Storage objects) that predates IAM. Google recommends IAM-only (โuniform bucket-level accessโ) for new resources.
Can a single principal hold conflicting roles? IAM is purely additive โ a principalโs effective permissions are the union of every role granted directly or inherited. Thereโs no โdeny by default when roles conflictโ; the only way to explicitly block a permission is a Deny policy.
How is IAM different from Cloud Identity? IAM controls what an already-authenticated principal (user, group, service account) can do to GCP resources. Cloud Identity is the identity provider that manages the users and groups themselves โ authentication happens in Cloud Identity; authorization happens in IAM.
Why does a service account sometimes have access I never explicitly granted? Almost always inherited from a folder or organization-level binding โ checking the full resource hierarchy above the resource in question, not just the resourceโs own policy, is the fix.
Do Deny policies replace the need for careful role scoping? No โ theyโre a backstop for guardrails that must never be violated, not a substitute for granting the right permissions in the first place. Relying on Deny policies to compensate for consistently over-broad grants elsewhere just moves the maintenance burden without reducing it.
Whatโs the practical difference between a predefined and custom role in day-to-day operations? A predefined role updates automatically as Google adds permissions to a service, which is usually what you want. A custom role gives you exact control but means youโre responsible for revisiting it whenever the service youโre scoping access to gains new capabilities you might need to include or explicitly exclude.
IAM vs Cloud Identity vs Workload Identity Federation
These three names get confused often enough to deserve a direct comparison. IAM answers โwhat can this already-identified principal do.โ Cloud Identity answers โwho is this principal, and what groups do they belong toโ โ itโs the identity layer underneath IAM. Workload Identity Federation answers a third question entirely: โhow does a workload running outside GCP (in AWS, on-prem, or in GitHub Actions) authenticate to GCP without a long-lived service account key.โ Understanding that these solve different layers of the same overall problem โ rather than treating them as interchangeable security features โ makes it much easier to reason about where a given access issue actually lives when something goes wrong.
Summary
IAMโs power and its risk come from the same source: inheritance makes broad access easy to grant and easy to forget about. The discipline that actually keeps a GCP environment secure isnโt a single configuration setting โ itโs the consistent habit of scoping roles to the narrowest resource and permission set that satisfies the real need, auditing whatโs actually being used versus whatโs been granted, and reserving Deny policies for the guardrails that must never be overridden by a well-intentioned but under-informed grant somewhere in the hierarchy.