Cloud/ Google Cloud / Identity & Security / Google Cloud IAM Explained: Roles, Policies, and the Resource Hierarchy

GCP Google Cloud Platform Guide 1 of 10 54 guides ยท updated 2026

Guides to BigQuery, Vertex AI, GKE, Dataflow, and the rest of Google's data- and AI-first cloud โ€” written for engineers shipping real workloads.

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 C

A 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:

Terminal window
# Grant a service account read access to a specific BigQuery dataset
gcloud 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 project
gcloud 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:

Terminal window
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.

Terminal window
gcloud iam deny-policies create block-external-sharing \
--organization=123456789 \
--rules-file=deny-rule.yaml
rules:
- 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:

Yes, matches

No match

Yes

No

API request from principal

Any applicable Deny policy?

Request denied โ€” stops here

Collect all Allow bindings

Resource-level policy

Project-level policy

Folder-level policy

Organization-level policy

Union of all granted permissions

Does union include required permission?

Request allowed

Request denied

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โ€:

Terminal window
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:

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


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.