# GCP IAM Project-Level Role Binding

> Project-level IAM bindings apply to every current and future resource in the project. ZopNight's RC-1251 flags each such role that could instead be bound on individual resources, and skips 13 non-scopable roles (Owner, billing, org-policy administration) where "move it to resource level" would be impossible advice.

Source: https://zop.dev/integrations/gcp/recommendations/gcp-iam-project-level-role-binding
Updated: 2026-08-19

---

## Project scope is a forward commitment

A role bound at project level does not just cover the resources that existed when someone
clicked Save. It covers every resource anyone creates in that project forever after. Grant
`roles/storage.admin` on the project because a pipeline needed one bucket, and next
quarter's new buckets, including the one holding customer exports, are silently included.
That forward-inheriting property is what separates a project binding from the same role on
a single bucket or Cloud SQL instance, and it is why blast-radius reviews treat the two
very differently even when the role name is identical.

## The 13 roles the rule refuses to flag

Advice is only useful if it is possible to follow, so the rule maintains an explicit skip
list of roles with no resource-level equivalent. Project Owner, Editor, and Viewer are
excluded here because RC-1250 already flags them with the correct remediation: replace,
not re-scope. A project-wide Owner grant cannot be "moved to a resource". The four
`roles/billing.*` roles and the project/organization administration roles
(`roles/resourcemanager.projectIamAdmin`, `roles/resourcemanager.organizationAdmin`,
`roles/orgpolicy.policyAdmin`, `roles/iam.organizationRoleAdmin`) plus the two
`roles/serviceusage.*` roles are inherently project- or org-scoped. What remains, the
findings you actually see, are roles like `roles/storage.admin` or `roles/cloudsql.admin`
that genuinely could live on the specific resources a member uses.

## Narrowing without breaking anything

Add the resource-level binding first, remove the project-level one second, and keep the
order strict, since the overlap window is what prevents an outage:

```bash
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --format="table(bindings.role, bindings.members)"
```

lists what is bound project-wide today; then, for example,
`gcloud storage buckets add-iam-policy-binding gs://BUCKET --member=... --role=...` creates
the narrow grant before you delete the broad one. Verify the workload with the narrow grant
under real traffic before removing anything.

## Provenance instead of a hardcoded label

Detection rides on how the rows are produced: the discoverer's project-policy walk is the
only producer of GCP `iam-role` resources, so the resource type itself proves the binding
is project-level, replacing an older label that was stamped true on every row and
therefore discriminated nothing. The rule then abstains on any role in the skip list. All
evidence is read from the project IAM policy; ZopNight never edits bindings, and the
finding retires on the discovery cycle after the project-level binding disappears.
