The Resource Hierarchy & IAM Inheritance
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.
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.
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 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 domain — hatch.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.
- Granting a role at the organization node to "save time" —
roles/editortogcp-developers@hatch.ioat the org means developers can edithatch-data-prodand 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.
- 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 likegcp-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.
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 tohatch-app-prodandhatch-app-stagingonly - 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