Google Cloud IAM Explained (2026): New Basic Roles, Deny and PAB

Nov 10, 2026 · 12 min · Ajay Kumar

The first Google Cloud IAM question I get from people coming over from AWS is always the same: where do I write the policy document? You don't, mostly. Google Cloud IAM is built around bindings of roles to principals on resources, inherited down a hierarchy, with a few newer policy types layered on top to take access away. This post explains how GCP IAM fits together in late 2026: the resource hierarchy, principals, basic vs predefined vs custom roles (including the new Admin, Writer and Reader roles that went GA on 29 October), service accounts and why keys are now off by default, Workload Identity Federation, IAM Conditions, deny policies, Principal Access Boundary and the org policies that are not IAM at all but still block you. If you are here because something is returning 403 right now, the companion guide, How to Fix Google Cloud 'Permission Denied' 403 Errors, is the faster read.

How Google Cloud IAM works: the resource hierarchy

Everything in Google Cloud sits in a tree. At the top is the organization (tied to a Google Workspace or Cloud Identity account), then optional folders, then projects, then the resources inside them: buckets, VMs, BigQuery datasets, Pub/Sub topics. The resource hierarchy docs describe this, and it matters because IAM attaches to it.

Each node can carry an allow policy: a list of role bindings, each saying "these principals get this role here". The IAM overview is explicit that descendants inherit their ancestors' allow policies, and that the effective allow policy on a resource is the union of its own policy and every ancestor's. The consequence catches people out: a binding lower down can only add access. If someone is a Storage Admin at the folder, nothing you write on the bucket removes that. Deny policies, covered below, changed that.

Allow policies have hard size limits. The IAM quotas page caps one allow policy at 1,500 principals across all bindings, counting domains and groups as one each. That is the main reason to grant roles to Google groups rather than individuals: you hit the ceiling far later, and access reviews become a group membership question.

Principals: users, groups, service accounts and federated identities

A principal is whoever is asking. The principals overview lists the types and the exact identifier syntax you put in a binding:

  • user:alex@example.com for a Google Account, group: for a Google group, domain: for every account in a Workspace or Cloud Identity domain.
  • serviceAccount:name@project.iam.gserviceaccount.com for a service account.
  • principal:// and principalSet:// identifiers for federated identities from workload and workforce identity pools.
  • allUsers and allAuthenticatedUsers, which mean the internet and anyone with a Google login respectively, and which you should treat as a public bucket waiting to happen.

Two additions from 2026 are worth knowing. Service account principal sets went GA on 3 March 2026, according to the IAM release notes, so principalSet://cloudresourcemanager.googleapis.com/projects/PROJECT_NUMBER/type/ServiceAccount now names every service account in a project, which is mostly useful in deny policies. And agents built on Vertex AI Agent Engine get their own agent identity principals (GA in April 2026), with identifiers like principal://agents.global.org-ORG_ID.system.id.goog/resources/aiplatform/.... I covered why agents need their own identity in Non-Human Identity in 2026; Google is one of the few clouds that has put it into the IAM principal model itself.

Basic vs predefined vs custom roles

Permissions in Google Cloud look like storage.objects.get: service, resource, verb. You never grant a permission directly. You grant a role, which is a named bundle of permissions. There are three kinds, described in the roles overview.

Basic roles are the broad ones. Until this autumn that meant Owner, Editor and Viewer. On 29 October 2026 Google made three new basic roles generally available, Admin (roles/admin), Writer (roles/writer) and Reader (roles/reader), and now calls Owner, Editor and Viewer the legacy basic roles. Both sets still work. The practical difference is that the new ones support IAM Conditions and the legacy ones do not, so if you must grant something broad in a sandbox, the docs now recommend the new set. Either way, basic roles contain thousands of permissions and have no place in production.

Predefined roles are per-service and maintained by Google, for example roles/storage.objectViewer or roles/run.invoker.

Custom roles are your own bundles. They live at the organization (organizations/ORG_ID/roles/NAME) or project (projects/PROJECT_ID/roles/NAME) level, never on folders. Limits from the quotas page: 300 custom roles per organization, 300 per project, 3,000 permissions per role. A deleted custom role's ID cannot be reused for 44 days, which matters if your Terraform destroys and recreates them.

Role type Example ID Who maintains it Works with IAM Conditions Limit Source
Basic (current) roles/admin, roles/writer, roles/reader Google Yes n/a IAM release notes, 29 Oct 2026
Basic (legacy) roles/owner, roles/editor, roles/viewer Google No n/a Conditions overview
Predefined roles/storage.objectViewer Google, updated automatically Yes, on supported resource types n/a Roles overview
Custom projects/my-proj/roles/deployer You Yes 300 per org, 300 per project, 3,000 permissions each IAM quotas

Service accounts and keys: why keys are off by default now

A service account is an identity for software. Code running on Compute Engine, Cloud Run or GKE gets the attached service account's credentials from the metadata server, short-lived and rotated for you. The trouble has always been the other path: downloading a JSON key, a long-lived bearer credential that ends up in CI variables, laptops and, eventually, a public repository.

Google's answer is to make the bad path opt-in. Organizations created on or after 3 May 2024 are secure by default, with seven org policies enforced from day one, including iam.managed.disableServiceAccountKeyCreation (no new keys), iam.disableServiceAccountKeyUpload, iam.automaticIamGrantsForDefaultServiceAccounts (the default Compute Engine service account no longer gets Editor on the project) and iam.allowedPolicyMemberDomains (no grants to identities outside your domain). Older organizations have none of these unless someone turned them on. If your org predates May 2024, check.

The default Compute Engine service account, PROJECT_NUMBER-compute@developer.gserviceaccount.com, deserves a specific mention. The Compute Engine service account docs note that in older setups it may hold Editor on the whole project and is attached to every VM created without an explicit account. That is how a single compromised VM becomes a compromised project. Create a dedicated service account per workload with only the roles it needs.

When a human needs to act as a service account, the supported route is impersonation, not a key. You grant the person roles/iam.serviceAccountTokenCreator on that one service account (it contains iam.serviceAccounts.getAccessToken), and they run gcloud with --impersonate-service-account, as the impersonation docs show. Each service account can hold at most 10 keys, per the quotas page, but the right number is zero.

Workload Identity Federation: keys for things outside Google Cloud

The remaining excuse for keys was workloads outside Google Cloud: GitHub Actions, AWS, on-premises Kubernetes. Workload Identity Federation removes it. You create a workload identity pool, add a provider (OIDC, SAML 2.0, AWS or X.509), map the external token's claims to Google attributes with CEL, and the Security Token Service swaps the external token for a short-lived Google one using RFC 8693 token exchange.

You can then grant roles directly to the federated principal, which Google now recommends, using identifiers such as:

principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/subject/SUBJECT
principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.repository/my-org/my-repo

Note the project number, not the project ID. The older pattern, granting the external identity roles/iam.workloadIdentityUser on a service account and impersonating it, still works and is needed for the few APIs that do not accept federated principals. Always set an attribute condition on the provider; a pool that trusts every GitHub repository is a pool that trusts strangers. The GKE flavour uses the pool PROJECT_ID.svc.id.goog and subjects like ns/NAMESPACE/sa/KSA, per the GKE workload identity concepts page. I went deeper on the protocol in Workload Identity Federation Explained, and the step-by-step for CI is in GitHub Actions OIDC With AWS, GCP and Azure.

IAM Conditions: attribute-based grants

An IAM Condition is a CEL expression attached to a role binding. The binding only applies when the expression is true. The conditions overview lists what you can test in allow policies: resource name, type and service, resource tags, request time, and destination IP and port for IAP. Typical uses are time-boxed access, prefix-scoped Cloud Storage access and "only resources tagged env=dev".

The limits are where people trip. Conditions cannot be used with the legacy basic roles or on bindings to allUsers and allAuthenticatedUsers. A conditional binding does not override an unconditional binding for the same role, so if the person already has the role unconditionally, your expiry does nothing. Google recommends no more than 100 conditional bindings per allow policy, and a single condition can contain at most 12 logical operators.

Here is a time-boxed grant and a deny policy that blocks service account key creation project-wide except for a break-glass group. The commands match the add-iam-policy-binding reference and the deny access docs; the current Google Cloud CLI is 592.0.0, released 3 November 2026.

# Google Cloud CLI 592.0.0. Grant read access to a contractor until 31 Dec 2026.
gcloud projects add-iam-policy-binding my-project \
  --member='user:contractor@example.com' \
  --role='roles/storage.objectViewer' \
  --condition='expression=request.time < timestamp("2027-01-01T00:00:00Z"),title=expires-2026'

# Deny policy: nobody creates service account keys in this project,
# except members of the break-glass group. Needs roles/iam.denyAdmin.
cat > deny-sa-keys.json <<'EOF'
{
  "displayName": "No SA keys except break-glass",
  "rules": [{
    "denyRule": {
      "deniedPrincipals": ["principalSet://goog/public:all"],
      "exceptionPrincipals": ["principalSet://goog/group/break-glass@example.com"],
      "deniedPermissions": ["iam.googleapis.com/serviceAccountKeys.create"]
    }
  }]
}
EOF
gcloud iam policies create deny-sa-keys \
  --attachment-point=cloudresourcemanager.googleapis.com/projects/my-project \
  --kind=denypolicies \
  --policy-file=deny-sa-keys.json

Deny policies and Principal Access Boundary: taking access away

Deny policies attach to an organization, folder or project and are inherited downward. The deny overview says IAM checks relevant deny policies before allow policies, so a denied permission stays denied whatever roles the principal holds. Rules name deniedPrincipals, optional exceptionPrincipals, deniedPermissions in the v2 format (iam.googleapis.com/roles.create, note the service FQDN) and optional exceptionPermissions. Conditions in deny rules can only test resource tags. Not every permission can be denied; check the supported permissions list before you design around one. Limits: 500 deny policies per resource and 500 rules across them.

Principal Access Boundary (PAB), GA since 16 December 2024, comes at the problem from the other side. Instead of attaching to a resource, a PAB policy is bound to a set of principals (a workforce pool, a workload pool, a Workspace domain, or everyone in a project, folder or organization) and lists the resources those principals are eligible to touch. The PAB docs spell out the key properties: it can never grant access, only make it ineligible; if no PAB policy applies, nothing is restricted; it only covers permissions in its enforcement version; and it fails closed, so an evaluation error denies the request. The use case is the one AWS solves with permission boundaries: "identities from this pool can only ever touch resources in these two projects", however many roles someone grants by mistake.

Google Cloud IAM: three policy types, one answer Request principal + permission + resource 1. PAB policy Is the resource eligible for this principal? 2. Deny policy Does a deny rule match on resource or an ancestor? 3. Allow policy Does a binding grant it here or on an ancestor? Allowed only if all three checks agree ineligible: 403 denied: 403 no binding: 403 Separate from IAM: organization policies (for example no SA keys, domain-restricted sharing) and VPC Service Controls perimeters can block a call that all three IAM checks allow. Policy changes typically propagate in 2 minutes, sometimes 7 or more.
How the three IAM policy types combine on every request, based on Google's deny overview, PAB documentation and access change propagation page.

Org policies and VPC Service Controls are not IAM (but they still say no)

Two more systems block requests and are routinely mistaken for IAM. Organization policies constrain what can be configured, not who can call what: allowed regions, no external IPs, no service account keys, no grants outside your domain. Google's domain-restricted sharing docs now label iam.allowedPolicyMemberDomains a legacy constraint and offer the managed iam.managed.allowedPolicyMembers constraint alongside it. When one fires, being Owner does not help, because the constraint is evaluated regardless of your roles.

VPC Service Controls draw a perimeter around projects and services and block data movement across it. A blocked call returns an error like "Request is prohibited by organization's policy" with a vpcServiceControlsUniqueIdentifier, as the VPC SC troubleshooting page shows, and the fix is an ingress or egress rule, not a role.

Finally, timing. Allow and deny changes typically take effect in about 2 minutes and can take 7 minutes or longer, and group membership changes can take hours, per access change propagation. A lot of "IAM is broken" tickets are really "IAM is eventually consistent". In the AI era this matters more, not less: an agent that retries a 403 immediately after granting itself a role will conclude the grant failed. Google shipped an IAM MCP server on 10 September 2026 so AI tools can inspect custom roles and deny policies, which is useful, and also a reason to make sure those tools run with Reader-level roles and a PAB policy around them.

FAQ

What is the difference between basic, predefined and custom roles in GCP?

Basic roles (Admin, Writer, Reader, and the legacy Owner, Editor, Viewer) grant thousands of permissions across every service. Predefined roles are scoped to one service and maintained by Google. Custom roles are bundles you define at organization or project level, up to 300 per scope and 3,000 permissions each.

Should I use roles/admin or roles/owner?

For new grants, Google recommends the new basic roles that went GA on 29 October 2026, because they support IAM Conditions and the legacy ones do not. In production, prefer predefined or custom roles over either.

Can a lower-level IAM policy remove access granted higher up?

No. The effective allow policy is the union of a resource's policy and all its ancestors' policies, so lower bindings only add access. To remove access, use a deny policy at the right level or a Principal Access Boundary policy on the principals.

How is GCP IAM different from AWS IAM?

AWS attaches JSON policy documents to identities and resources and evaluates explicit denies across several policy types. Google Cloud binds roles to principals on nodes of a resource hierarchy, with deny and PAB policies added later. My AWS IAM explainer covers the AWS side for comparison.

This quarter, on any Google Cloud organization I was responsible for, I would do three things. Check whether the org predates May 2024 and, if so, enforce the secure-by-default constraints, after listing what each would break, starting with key creation and default service account grants. Replace every remaining service account key used by CI with Workload Identity Federation and an attribute condition. And write one deny policy, the key-creation one above, because it is cheap, it is inherited everywhere, and it stops the most common way a GCP project leaks.

Related: Workload Identity Federation Explained (2026): OIDC Instead of Cloud Keys, AWS IAM Explained (2026): Users vs Roles, 9 Policy Types, SCPs and RCPs and How to Use GitHub Actions OIDC With AWS, GCP and Azure (2026).

Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related