Chapter 8: Identity & Access on GCP
Topic 48

The Resource Hierarchy & IAM Inheritance

ConceptIAM

GCP arranges everything in a tree — the organization at the root, then folders, then projects, then the resources inside them — and an IAM policy set on any node is inherited by everything beneath it. This is the single most important model in GCP access control, and it is the one AWS engineers get wrong first: there is no per-account boundary that stops a grant. A role granted at the org node applies in every folder, every project, and every resource under it.

Inheritance is purely additive. A child can never subtract a permission an ancestor granted — there is no "deny by being lower in the tree." The effective access on any resource is the union of its own policy and every ancestor's policy up to the org. Get this one idea straight and the rest of the chapter has a frame; miss it and you will grant a convenient role at the wrong node and hand a group access to projects that do not exist yet.

The Four Levels

The hierarchy is organization → folder → project → resource. The organization is the root, one per company, created from and tied to a Cloud Identity or Google Workspace domain — for us, hatch.io, with the numeric ID organizations/123456789012. Folders group projects by team or environment; we use platform/, apps/, and data/. The project is the IAM and billing boundary, where most resources actually live — hatch-app-prod, hatch-data-prod. And individual resources, a bucket or a Pub/Sub topic, can carry their own policy at the leaf.

The org resource is not optional scaffolding. You cannot have a folder-and-project tree without it, and it is created from the identity domain rather than declared from nothing. Folders exist to be IAM and policy boundaries — a folder with no grants attached to it is doing no work. The whole point of placing hatch-app-prod and hatch-app-staging under an apps/ folder is so a role can attach to that folder and reach both.

Policies Attach to Nodes, Permissions Flow Down

An IAM policy is a set of bindings attached to exactly one node. The effective policy on any resource is the union of its own policy plus every ancestor's policy, merged up to the org. Terraform manages each node's policy separately — a binding on the apps/ folder and a binding on hatch-app-prod are two different resources in your config — but the runtime result is the merge of both, plus whatever the org grants.

The configuration below grants roles/run.developer to the developers group on the apps/ folder. Because policies flow down, that one binding reaches hatch-app-prod and hatch-app-staging without a per-project copy. It deliberately does not reach hatch-data-prod, which lives under the data/ folder — choosing the node is the access-control decision.

A folder-level grant reaches every project beneath it
resource "google_folder_iam_member" "apps_run_developer" {
  folder = google_folder.apps.name        # the apps/ folder node
  role   = "roles/run.developer"
  member = "group:gcp-developers@hatch.io"   # a group, never a user
}
# Effective on hatch-app-prod AND hatch-app-staging (both under apps/),
# but NOT on hatch-data-prod, which lives under the data/ folder.

Read that as a statement about the tree, not about a project. The grant lands on the folder; every current and future project under apps/ inherits it. Move a project under a different folder and the inherited grants change with it — the policy followed the node, not the project name.

Additive-Only Inheritance

There is no mechanism that says "this binding applies less because it sits lower in the tree." A binding at the org grants everywhere beneath it, full stop, and a narrower grant on a child only adds to that — it cannot shrink it. If you grant roles/editor to a group at the org and then grant the same group roles/viewer on one project hoping to scope them down, the editor grant still applies on that project. You added viewer on top of editor; you took nothing away.

The only ways to carve out an exception to an inherited allow are an IAM deny policy or an IAM condition, both covered later in this chapter. Neither is a smaller positive grant on a child, because a smaller positive grant cannot subtract. When you catch yourself trying to "scope down" by granting less on a child, stop — that is the additive model telling you the fix lives at the ancestor or in a deny policy.

A grant on apps/ flows down — never up, never sideways
Organization
hatch.io · organizations/123456789012
Folder · grant attaches here
apps/ — roles/run.developer → gcp-developers@hatch.io
Project · inherits ✓
hatch-app-prod
Project · inherits ✓
hatch-app-staging
Resource · own policy + inherited
a bucket or Pub/Sub topic
Folder · NOT reached ✗
data/ — hatch-data-prod (no run.developer)

Granting at the Right Level

Picking the node is the real design work, and the rule is to grant each role at the highest node where it is still correct and no higher. roles/billing.admin for gcp-billing-admins@hatch.io belongs at the org or the billing-account level, because billing is genuinely org-wide. roles/run.developer for gcp-developers@hatch.io belongs on the apps/ folder, so it reaches both app projects but never touches hatch-data-prod. The org node is reserved for break-glass admin groups like gcp-org-admins@hatch.io and nothing else.

The failure that this prevents has a familiar shape. Someone grants roles/editor to a group at organizations/123456789012 to "save time," and that group silently has editor on every project the org will ever contain, including ones created next year. The blast radius is the whole tree, forever, until someone runs an audit and finds it. Grant at the right level the first time and there is nothing to find.

The GCP Org vs the Cloud Identity Domain

The GCP organization resource — the root node of the resource hierarchy (organizations/123456789012), the thing IAM policies and folders attach to. It is created from your identity domain and cannot exist without one.

The Cloud Identity / Workspace domainhatch.io, where your users and groups actually live (gcp-developers@hatch.io). It is the identity source; the GCP org is the resource-hierarchy root built from it. Confusing the two leaves you unable to explain why "the org" both holds projects and defines people.

Common Mistakes
  • Granting a role at the organization node to "save time" — roles/editor to gcp-developers@hatch.io at the org means developers can edit hatch-data-prod and every future project, and nobody notices until a security review finds it.
  • Expecting a narrower grant on a child project to override or shrink an ancestor grant — inheritance is additive, so the org-level binding still applies and the child binding only adds more.
  • Designing folders as cosmetic grouping with no IAM intent, then having nowhere clean to attach a folder-scoped role and falling back to per-project copy-paste bindings that drift apart over time.
  • Confusing the org node with the Google Workspace or Cloud Identity account — the GCP organization resource is created from and tied to that identity domain, and you cannot have a folder-and-project tree without it.
  • Trying to "scope down" inherited access by granting a smaller role on a child instead of using a deny policy or condition — the smaller grant adds, it never subtracts, so the broad inherited access remains.
Best Practices
  • Grant each role at the highest node where it is still correct and no higher — the apps/ folder for app-developer roles, the org only for genuinely org-wide roles like gcp-org-admins@hatch.io.
  • Use folders deliberately as IAM boundaries (platform/, apps/, data/) so every environment- or team-scoped role has an obvious place to attach.
  • Audit org-node and folder-node bindings on a schedule with gcloud asset analyze-iam-policy — these are the grants with the largest blast radius.
  • Reserve broad ancestor grants for break-glass admin groups only, and keep everything else scoped to the project or folder that actually needs it.
  • Grant to groups (group:...@hatch.io), never to individual users, so membership changes never require a Terraform change to a binding.
Comparable models AWS Organizations + SCPs gate accounts, but IAM does not inherit down them the way GCP policies do Azure management groups → subscriptions → resource groups the closest structural analog Config Connector · gcloud operate on the same GCP hierarchy

Knowledge Check

Why does a role granted to a group at organizations/123456789012 reach every project in the org?

  • IAM policies inherit down the hierarchy, so an org-node binding applies to every folder, project, and resource beneath it — including ones created later
  • Org-level grants are physically copied into each project's own IAM policy document at project creation time, and any project owner can later remove their local copy
  • Only projects in the same folder as the org node inherit the grant
  • The grant reaches projects only if they explicitly opt in to inheritance

A group has roles/editor at the org. You grant it roles/viewer on one project to "scope it down" there. What is the effective access on that project?

  • Editor still applies — inheritance is additive, so the child grant only adds viewer on top and removes nothing
  • Viewer, because the more specific child-project grant takes precedence over and overrides the inherited org-level editor
  • Neither, because two roles for the same member conflict and cancel out
  • Viewer everywhere and editor nowhere, because the last grant wins

Where should roles/run.developer for gcp-developers@hatch.io be granted so it reaches the app projects but not hatch-data-prod?

  • On the apps/ folder, so it flows down to hatch-app-prod and hatch-app-staging only
  • At the org node, since that is the simplest single place to grant it
  • On each individual resource inside the app projects, granting one binding at a time by hand
  • On the data/ folder, which the app projects inherit from

What is the difference between the GCP organization resource and the hatch.io Cloud Identity domain?

  • The Cloud Identity domain holds users and groups; the GCP org is the resource-hierarchy root created from that domain, and it cannot exist without one
  • They are two names for the same single object; "organization" is simply the label the GCP Console shows for the underlying Cloud Identity domain you already created
  • The GCP org holds users and groups; the Cloud Identity domain holds projects
  • The Cloud Identity domain is optional and only needed for Workspace email

You got correct