GCP Identity

Google Cloud IAM Fundamentals: Roles, Service Accounts, Policy & Inheritance

Every other thing you build on Google Cloud — a virtual machine, a bucket, a database, a pipeline — sits behind one question Google asks on every single API call: is this caller allowed to do this on this resource? The system that answers it is Identity and Access Management (IAM), and it is the one service you cannot skip. Get IAM right and the rest of the platform is just resources. Get it wrong and you have either an outage (nobody can do their job) or a breach (everybody can). This lesson takes you from “what is a principal” to confidently reading an allow policy, reasoning about inheritance, conditioning a grant, blocking an action with a deny policy, and — the part most newcomers get dangerously wrong — handling service accounts without ever downloading a key. By the end you will be able to design least-privilege access on GCP the way it is actually done in production, and answer the IAM questions that come up in every Associate Cloud Engineer interview.

Learning objectives

By the end of this lesson you will be able to:

Prerequisites & where this fits

You need a Google Cloud account, the gcloud CLI (or Cloud Shell, where it is pre-installed), and a basic grasp of the resource hierarchy — Organization → Folders → Projects → Resources — which we cover in the previous lesson, Google Cloud Fundamentals: Global Infrastructure, Resource Hierarchy & Pricing. That hierarchy is not background detail here: it is the thing IAM policies attach to, and inheritance flows along it. This is the Identity module of the Google Cloud Zero-to-Hero course and the foundation for everything that follows, especially the three-tier application lesson next. No prior IAM experience is assumed; if you know AWS IAM, note the explicit contrasts — the models differ in ways that catch people out.

Core concepts: the three questions IAM answers

IAM is, at heart, a function with three inputs and one output. Who (the principal) wants to do what (the permission, expressed via a role) on which resource? The answer is allow or deny. Internalise these terms before anything else:

Term What it is Example
Principal (member) The identity making the request user:alex@example.com, a service account, a group
Permission The finest-grained right to call one API method storage.objects.get, compute.instances.start
Role A named bundle of permissions you grant (you never grant permissions individually) roles/storage.objectViewer
Allow policy The list of bindings (member → role) attached to a resource “alex has Storage Object Viewer on bucket X”
Resource hierarchy Organization → Folders → Projects → Resources A policy at a folder is inherited by every project beneath it

Three rules follow from this and they are the whole game:

  1. You grant roles, not permissions. A role is a collection of permissions. You attach roles to principals on a resource; you cannot bind a bare permission.
  2. Policies attach to resources, at any level of the hierarchy. You can grant a role at the organization, a folder, a project, or many individual resources (a bucket, a Pub/Sub topic).
  3. Inheritance is additive and downward. A grant at a parent flows to every child. The effective access of a principal is the union of every grant at the resource and all its ancestors. We expand on this — and on why it is so different from AWS — below.

Principals: the four kinds of identity

A principal (the API still calls it a “member” in policy JSON) is who is asking. There are four kinds you will meet, plus a couple of special set-based ones.

Principal type Policy prefix What it represents When to use
Google account user: A human, identified by an email (Gmail or a Cloud Identity / Workspace account) Granting access to an individual person — sparingly; prefer groups
Google group group: A collection of accounts and service accounts managed in Cloud Identity / Workspace The default for human access — grant to the group, manage membership outside IAM
Service account serviceAccount: A non-human identity for an application, VM, or workload Anything code-driven: a VM, a Cloud Run service, a CI job
Cloud Identity / Workspace domain domain: Everyone in an entire domain Broad, coarse grants — use with care
All authenticated / all users allAuthenticatedUsers, allUsers Any Google identity, or literally anyone on the internet Public resources only (e.g. a public website bucket); allUsers makes data public — treat as radioactive
Federated identity set principalSet:// External identities mapped in via Workload Identity Federation Keyless access from CI/CD and other clouds (covered at the end)

The single most important habit a junior engineer can build: grant to groups, not to individual users. When someone joins or leaves a team you change one group membership instead of hunting through dozens of policies across the hierarchy. Reserve user: grants for genuine exceptions, and treat allUsers as a deliberate, reviewed decision to make something public.

Cloud Identity is the free identity service that gives your organisation managed Google accounts and groups even if you do not use Google Workspace. It is what turns “a pile of personal Gmail accounts” into “a governed directory” — and it is the prerequisite for having an Organization node at all.

Roles: basic, predefined, and custom

A role is a bundle of permissions, and there are exactly three kinds. Choosing the right kind is most of least-privilege.

Role kind Examples Granularity Scope of permissions When to use
Basic (a.k.a. primitive) roles/owner, roles/editor, roles/viewer Extremely broad — span all services Project-wide read / read-write / full control Avoid in production. Fine for a personal sandbox only
Predefined roles/storage.objectViewer, roles/compute.instanceAdmin.v1, roles/bigquery.dataEditor Service- and task-scoped, curated by Google Just the permissions for one job on one service The default choice — start here, almost always
Custom roles/myorg.bucketLifecycleManager (you name it) Exactly the permissions you list Whatever set you assemble When no predefined role fits the least-privilege need

Basic roles are an anti-pattern and you should be able to say why in an interview. roles/owner and roles/editor carry thousands of permissions across every service — including, for Owner, the ability to change IAM itself. Granting Editor “just to get someone unblocked” hands them write access to your databases, networks, and (in many cases) the keys to escalate further. They also cannot be constrained with IAM Conditions (below). The professional move is to grant a predefined role scoped to the task, and only drop to a custom role when even the narrowest predefined role grants more than needed.

Custom roles carry a small operational cost: you own them. When Google adds a new permission to a service, predefined roles get it automatically; your custom role does not, so you must maintain it. Define custom roles at the organization or project level, version them, and keep them few. A useful workflow is to start from a predefined role, see what it grants, and trim — Google’s Role Recommender (part of the IAM Recommender / Active Assist) will even suggest tighter roles based on the permissions a principal has actually used over the last 90 days.

# Inspect exactly what a role grants before you hand it out
gcloud iam roles describe roles/storage.objectViewer

# Create a custom role at the project level from a list of permissions
gcloud iam roles create bucketLifecycleManager \
  --project=my-prod-project \
  --title="Bucket Lifecycle Manager" \
  --permissions=storage.buckets.get,storage.buckets.update \
  --stage=GA

The allow policy and how a request is evaluated

The allow policy (returned by getIamPolicy, historically just “the IAM policy”) is a list of bindings. Each binding pairs one role with a set of members and, optionally, a condition. Here is a real one for a project:

{
  "bindings": [
    {
      "role": "roles/storage.objectViewer",
      "members": [
        "group:data-readers@example.com",
        "serviceAccount:report-job@my-prod-project.iam.gserviceaccount.com"
      ]
    },
    {
      "role": "roles/compute.instanceAdmin.v1",
      "members": ["group:platform-team@example.com"],
      "condition": {
        "title": "nonprod-only",
        "expression": "resource.matchTag('123456789012/environment', 'nonprod')"
      }
    }
  ]
}

When a principal calls an API, Google evaluates the request roughly like this:

  1. Gather deny policies at the resource and all ancestors. If any matching deny rule applies and no exception covers the caller, the request is denied immediately — deny always wins.
  2. Gather allow policies at the resource and all ancestors and take their union.
  3. If some binding grants the required permission and its condition (if present) evaluates true, the request is allowed. Otherwise the default is deny.

You manage bindings with add-iam-policy-binding / remove-iam-policy-binding (which read-modify-write the policy for you) — almost never by hand-editing the JSON:

# Grant a group a predefined role on a project
gcloud projects add-iam-policy-binding my-prod-project \
  --member="group:data-readers@example.com" \
  --role="roles/storage.objectViewer"

# Read the allow policy AT THIS NODE (does NOT show inherited bindings)
gcloud projects get-iam-policy my-prod-project --format=json

That parenthetical is the trap that snares everyone new to GCP, and it leads straight into inheritance.

Policy inheritance: additive, a union, and not reducible by a child

This is the concept the brief calls out, the one that separates people who think they understand GCP IAM from people who actually do.

A policy set at a parent node is inherited by every descendant. Grant roles/viewer at a folder and every project, and every resource in those projects, inherits Viewer. The principal’s effective access on any resource is the union of every binding at that resource and at all of its ancestors — resource, then project, then folder(s), then organization. It is purely additive: each level can only add access.

The consequence that bites: a child cannot take away what a parent granted. If someone has roles/editor at the folder, granting them a tiny roles/storage.objectViewer at one project beneath does not scope them down — they still have Editor there, because the union includes the folder grant. There is no “more specific binding wins” and no inheritance block on the allow side. If you need to remove a permission that a broad ancestor grant confers, the allow model cannot do it; that is precisely the job of a deny policy (next section).

# The truth about a principal's access has to be read across the WHOLE chain,
# not from a single node's getIamPolicy. Policy Analyzer does that:
gcloud asset analyze-iam-policy \
  --organization=123456789012 \
  --identity="user:alex@example.com"

Contrast with AWS — the difference that catches people out

If your mental model comes from AWS, retune it deliberately. The differences are not cosmetic:

Concept Google Cloud IAM AWS IAM
Where the policy lives On the resource (the project/folder/org/resource node) — “resource-centric” Primarily on the identity (a user/role gets attached policies) — “identity-centric”
Inheritance Automatic and additive down the hierarchy (org → folder → project → resource); a parent grant is inherited and cannot be reduced by a child No automatic grant inheritance; Service Control Policies (SCPs) at OUs set a permission ceiling (they can only restrict, never grant)
Default Implicit deny; an allow anywhere in the chain wins Implicit deny; an explicit Deny overrides any Allow
Scoping down Use IAM Conditions (per-binding) and deny policies; you cannot subtract via a child allow Use SCPs (ceiling) plus permission boundaries on identities
“Override” mechanism Deny policy (separate resource, evaluated first) Explicit Deny statement in any policy

The one-sentence version for an interview: GCP IAM is resource-centric with additive, automatic inheritance where allow grants accumulate down the hierarchy and a child cannot revoke a parent’s grant — so you constrain with Conditions and deny policies, whereas AWS is identity-centric where SCPs set a ceiling and an explicit Deny overrides an Allow.

IAM Conditions: bounding a grant with CEL

The allow model is additive, but you are not stuck with all-or-nothing roles. IAM Conditions attach a CEL (Common Expression Language) predicate to a single binding, so the grant only takes effect when the expression is true. This is the scalpel for least privilege on the allow side. Conditions match three attribute families:

Attribute family What you can match on Typical use
Resource resource.name, resource.type, resource.matchTag(...) Limit a role to specific resources or to resources carrying a tag
Date/time request.time Self-expiring grants — the backbone of just-in-time elevation
Request request.path, request properties Match on the call itself

Two everyday examples:

# Time-bound: this compute.admin grant self-expires — no cleanup needed
gcloud projects add-iam-policy-binding my-prod-project \
  --member="user:alex@example.com" \
  --role="roles/compute.admin" \
  --condition='expression=request.time < timestamp("2026-07-01T00:00:00Z"),title=temp-compute-admin,description=Expires 2026-07-01'

# Resource-bound: storage admin only on buckets whose name starts with prod-logs-
gcloud projects add-iam-policy-binding my-prod-project \
  --member="group:storage-ops@example.com" \
  --role="roles/storage.admin" \
  --condition='expression=resource.name.startsWith("projects/_/buckets/prod-logs-"),title=only-prod-logs'

Caveats worth memorising, because they appear in exams and in real debugging: CEL here is a deliberately limited surface (no arbitrary functions); use ==, never =; basic roles cannot be conditioned at all (another reason to avoid them); and resource.name/resource.type are not populated for every service, so test the condition against the real API before trusting it. Conditions are explored in depth — alongside deny policies and impersonation — in the companion lesson Advanced GCP IAM: Deny Policies, Conditional Bindings, and Impersonation Chains.

Deny policies: the hard override

Because allow inheritance can only add access, you need a way to say “no, regardless of any allow grant anywhere.” That is the deny policy — a separate resource from the allow policy, evaluated first, that overrides allow. It is how you claw back a permission that a broad ancestor binding would otherwise confer.

A deny policy attaches to an attachment point (an org, folder, or project, in a URL-encoded form) and contains rules with deniedPrincipals, optional exceptionPrincipals, the deniedPermissions, and an optional denialCondition:

# deny-destructive.yaml — nobody may delete projects or buckets, except break-glass
rules:
- denyRule:
    deniedPrincipals:
    - "principalSet://goog/public:all"
    exceptionPrincipals:
    - "principalSet://goog/group/breakglass-admins@example.com"
    deniedPermissions:
    - "cloudresourcemanager.googleapis.com/projects.delete"
    - "storage.googleapis.com/buckets.delete"
gcloud iam policies create deny-destructive \
  --attachment-point="cloudresourcemanager.googleapis.com/folders/456789012345" \
  --kind=denypolicies \
  --policy-file=deny-destructive.yaml

Three things that bite if you ignore them. First, deny policies act on permissions (e.g. storage.googleapis.com/buckets.delete), not roles — so they cut across every role that contains that permission, which is exactly what makes them powerful. Second, principalSet://goog/public:all means every principal, including you — always pair a broad deny with exceptionPrincipals for the break-glass identities, or you will lock yourself out. Third, an exception principal is an escape hatch from the deny, not a grant — that principal still needs an allow binding for the action to actually succeed. Used well, deny policies are how you enforce “no one deletes production, full stop” in a way no accidental Editor grant can undo.

Service accounts: the identity for your code

A service account (SA) is a special principal that is not a person — it is the identity an application, VM, or workload uses to authenticate to Google APIs. It has an email (my-app@my-project.iam.gserviceaccount.com) but no password and no interactive login. SAs are simultaneously a principal (you grant it roles, so it can do things) and a resource (you grant principals roles on it, so they can use or manage it). That dual nature is the source of most service-account confusion, so hold it firmly.

There are two broad kinds:

Service account kind Created by Notes
User-managed You (gcloud iam service-accounts create ...) The ones you create for your apps; you control their roles and lifecycle. Up to 100 per project by default
Default Google, automatically (e.g. the Compute Engine default SA PROJECT_NUMBER-compute@developer.gserviceaccount.com) Created with broad Editor by default — a classic over-privilege. Disable the automatic Editor grant via org policy and assign purpose-built SAs instead
Google-managed service agents Google Used by Google services to act on your behalf; you generally do not manage these directly

The default Compute Engine SA being granted Editor is a textbook over-privilege you should flag in any review: every VM in the project then runs as an identity that can edit almost everything. The fix is to create a dedicated SA per workload with only the roles it needs, attach that to the VM/Cloud Run service, and use the org policy constraint iam.automaticIamGrantsForDefaultServiceAccounts to stop the broad default grant.

Keys versus impersonation — the most important choice you will make

There are two ways to use a service account, and choosing correctly is the single highest-leverage security decision a junior engineer makes on GCP.

Approach What it is Lifetime Verdict
Downloaded SA key A JSON file (keys create) containing a private key — a bearer credential Never expires until you revoke it Avoid. The number-one credential-leak vector — copyable, commit-able, and context-free
Impersonation A caller with the right role mints a short-lived token for the SA on demand, no key file ~1 hour (extendable to 12h via org policy) Prefer. Nothing to store, rotate, or leak
Attached SA A VM / Cloud Run / GKE workload runs as an SA; the platform supplies credentials automatically via the metadata server Auto-rotated by the platform Prefer for workloads on GCP — zero key handling

A downloaded key has no expiry, no audience binding, and no idea where it is being used; a key committed to a repo in 2022 still works today. Impersonation deletes that whole class of risk. The enabling role is roles/iam.serviceAccountTokenCreator, granted on the target SA (remember: the SA as a resource) to the caller:

gcloud iam service-accounts add-iam-policy-binding \
  deploy-sa@my-prod-project.iam.gserviceaccount.com \
  --member="group:platform-deployers@example.com" \
  --role="roles/iam.serviceAccountTokenCreator"

# Now any member runs gcloud AS the SA, no key file anywhere:
gcloud storage ls --impersonate-service-account=deploy-sa@my-prod-project.iam.gserviceaccount.com

Treat serviceAccountTokenCreator with the same suspicion as roles/owner: whoever holds it on an SA becomes that SA and inherits all its permissions. It is a privilege-escalation primitive by design, so grant it narrowly and audit it.

iam.serviceAccounts.actAs — the permission that lets you attach an SA

There is a second, subtler control you must understand: to deploy a resource that runs as a service account — attach an SA to a VM, a Cloud Run service, a Cloud Function — the deploying principal needs the iam.serviceAccounts.actAs permission on that SA (typically via roles/iam.serviceAccountUser). This exists to prevent privilege escalation: without it, anyone who could create a VM could simply attach your most powerful SA and inherit its rights. actAs is the gate that says “you are allowed to run code as this identity.”

# Let a deployer attach this SA to workloads (VMs, Cloud Run, Functions)
gcloud iam service-accounts add-iam-policy-binding \
  app-runtime-sa@my-prod-project.iam.gserviceaccount.com \
  --member="group:app-deployers@example.com" \
  --role="roles/iam.serviceAccountUser"

Distinguish the two clearly: serviceAccountUser / actAs lets you attach an SA to a workload (deploy-time); serviceAccountTokenCreator lets you mint tokens to impersonate an SA directly (run-time). Both are escalation paths, both are granted on the SA-as-resource, and both belong on every reviewer’s checklist.

Workload Identity Federation: keyless access from outside GCP

So workloads on GCP use an attached SA (no keys), and humans/automation impersonate (no keys). What about a workload outside GCP — a GitHub Actions pipeline, a GitLab runner, an app on AWS — that needs to call Google APIs? The wrong answer, and the most common one, is to download an SA key and paste it into a CI secret. The right answer is Workload Identity Federation (WIF).

WIF replaces the static key with a trust relationship. Your external system already proves its own identity to its own provider with a short-lived OIDC (or SAML) token; GCP’s Security Token Service (STS) exchanges that external token for a short-lived federated GCP credential — only when the claims inside the token match conditions you set (e.g. “this token came from my-org/my-repo on branch main”). No key is created, stored, or able to leak.

The moving parts:

Component What it is Lifetime
Workload identity pool A container for external identities, scoped to a project Permanent
Pool provider (OIDC/SAML) Trust config for one external issuer (GitHub, GitLab, AWS, etc.) Permanent
Attribute mapping Maps token claims (sub, repository) to Google attributes Config
Attribute condition A CEL expression that rejects tokens failing the predicate Config
STS token exchange The runtime swap of the external token for a GCP federated token ~1 hour

The federated identity is then bound to a service account via roles/iam.workloadIdentityUser — the federated cousin of tokenCreator — so the external identity can impersonate the SA and act with its roles. (Newer “direct resource access” lets you grant roles to the federated principalSet:// directly, skipping the SA, though not every client library honours federated principals yet.) The headline: no downloadable key ever exists. The full STS exchange, pool setup, and GitHub Actions worked example are in Keyless Authentication to GCP: Workload Identity Federation for GitHub Actions and CI/CD; the fundamentals point here is simply this is how outside workloads authenticate without keys, and you should reach for it by default.

Google Cloud IAM model & policy inheritance

The diagram ties the model together: principals on the left, the role → permission bundling in the middle, allow policies attached at organization, folder, project, and resource with grants accumulating downward as a union, the deny policy sitting in front as a hard override, and on the right the two keyless paths — workloads impersonating attached or target service accounts, and external identities entering through Workload Identity Federation.

Hands-on lab: roles, a service account, impersonation, and a condition

You will create a project-scoped custom role, a least-privilege service account, grant impersonation, prove you can act as the SA without a key, and add a self-expiring conditional grant. Run everything in Cloud Shell or a local shell where you have run gcloud auth login. This stays comfortably inside the GCP Free Tier / $300 credit — IAM operations themselves are free.

Step 1 — Set up variables

export PROJECT_ID="$(gcloud config get-value project)"
export SA_NAME="lab-app-sa"
export SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
echo "Project: $PROJECT_ID  SA: $SA_EMAIL"

Expected: your project ID and the service account email printed back.

Step 2 — Create a least-privilege service account

gcloud iam service-accounts create "$SA_NAME" \
  --display-name="Lab application SA"

gcloud iam service-accounts describe "$SA_EMAIL" \
  --format="value(email, disabled)"

Expected: the SA email followed by False (not disabled). You have created an identity for an app — note you did not create a key.

Step 3 — Grant the SA exactly one predefined role

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/storage.objectViewer" \
  --condition=None

Expected: the updated policy prints with a binding for roles/storage.objectViewer listing your SA. The SA can now read objects in any bucket in the project — and nothing else.

Step 4 — Grant yourself impersonation, then act as the SA with no key

export ME="$(gcloud config get-value account)"

gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
  --member="user:${ME}" \
  --role="roles/iam.serviceAccountTokenCreator"

# Mint a short-lived token AS the SA — note: no JSON key anywhere
gcloud auth print-access-token --impersonate-service-account="$SA_EMAIL" | head -c 12; echo " ...(token issued)"

Expected: a truncated access token followed by ...(token issued). You just authenticated as the service account using only your own identity plus the tokenCreator grant — exactly the pattern that makes downloaded keys unnecessary.

Step 5 — Add a self-expiring conditional grant

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="user:${ME}" \
  --role="roles/compute.viewer" \
  --condition='expression=request.time < timestamp("2026-12-31T00:00:00Z"),title=temp-compute-viewer,description=Self-expiring viewer'

Expected: the policy prints with a compute.viewer binding that carries a condition. This grant evaporates on its own at year-end — no cleanup task required.

Validation

# Confirm the SA's binding and your conditional binding both exist
gcloud projects get-iam-policy "$PROJECT_ID" \
  --flatten="bindings[].members" \
  --filter="bindings.members:(${SA_EMAIL} OR user:${ME})" \
  --format="table(bindings.role, bindings.members, bindings.condition.title)"

You should see roles/storage.objectViewer for the SA, roles/iam.serviceAccountTokenCreator (on the SA, shown via its own policy), and roles/compute.viewer with the temp-compute-viewer condition title for you.

Cleanup

# Remove the bindings you added, then delete the SA
gcloud projects remove-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${SA_EMAIL}" --role="roles/storage.objectViewer" --condition=None
gcloud projects remove-iam-policy-binding "$PROJECT_ID" \
  --member="user:${ME}" --role="roles/compute.viewer" \
  --condition='expression=request.time < timestamp("2026-12-31T00:00:00Z"),title=temp-compute-viewer,description=Self-expiring viewer'
gcloud iam service-accounts delete "$SA_EMAIL" --quiet

Cost note

Nothing in this lab costs money. IAM — roles, policies, bindings, service accounts, impersonation, conditions, deny policies, and Workload Identity Federation — is free; you pay for the resources IAM protects, not for IAM itself. The only thing to be tidy about is leftover service accounts and stale bindings, which are a security liability rather than a billing one. Deleting the SA removes the identity entirely.

Common mistakes & troubleshooting

Symptom Likely cause Fix
PERMISSION_DENIED even though you “granted the role” Granted at the wrong node, or the binding has a condition that evaluates false, or a deny policy blocks it Use gcloud asset analyze-iam-policy (Policy Analyzer) to see effective access across the whole chain; check conditions and deny policies
Scoped a user down with a narrow project grant but they still have too much Allow inheritance is additive — a broad ancestor grant (e.g. Editor at the folder) is not reduced by a child Remove the broad ancestor grant, or use a deny policy to override it; you cannot subtract via a child allow
iam.serviceAccounts.actAs error when deploying a VM/Cloud Run that runs as an SA Deployer lacks actAs on the target SA Grant roles/iam.serviceAccountUser on that SA to the deployer
Downloaded an SA key and it leaked / can’t be rotated cleanly Using a long-lived JSON key at all Switch to impersonation (on-GCP/local) and WIF (external CI); block keys with iam.disableServiceAccountKeyCreation
Every VM can edit everything Workloads running as the default Compute Engine SA with Editor Create a dedicated least-privilege SA per workload; disable the default Editor grant via org policy
Condition has no effect Applied to a basic role (can’t be conditioned), used = instead of ==, or matched an attribute the service doesn’t populate Use a predefined/custom role, fix the CEL operator, test the attribute against the real API
Locked out after adding a broad deny deniedPrincipals: public:all with no exception for your admins Add exceptionPrincipals for break-glass identities; remember exceptions still need an allow binding to act
getIamPolicy “doesn’t show” a grant you know exists It returns only this node’s bindings, not inherited ones Read the parent nodes too, or use Policy Analyzer for the effective union

Best practices

Security notes

IAM is your security perimeter on GCP, so a few principles carry outsized weight. Treat roles/owner, roles/iam.serviceAccountTokenCreator, and iam.serviceAccountUser as privilege-escalation primitives — whoever holds them can become other identities or change access itself; grant them rarely, to groups, and audit them like crown jewels. Eliminate standing credentials: downloaded SA keys are the most common breach vector on GCP precisely because they are long-lived and copyable, and impersonation plus WIF remove them entirely. Enforce least privilege structurally, not by good intentions: predefined/custom roles, Conditions to bound them, and deny policies to set hard limits. Make allUsers a reviewed decision — it makes data public to the entire internet. Finally, audit who did what with Cloud Audit Logs (Admin Activity logs are on by default and free), and revisit access with the IAM Recommender so permissions shrink toward what is actually used. The deep treatment of deny policies, conditions, and impersonation chains lives in the Advanced GCP IAM lesson.

Interview & exam questions

Quick check

  1. Name the four common principal types and give the policy prefix for each.
  2. Why are basic roles (Owner/Editor/Viewer) an anti-pattern, and what should you use instead?
  3. In one sentence, how does GCP allow-policy inheritance work, and how does it differ from AWS?
  4. A user has Editor at a folder; you grant Storage Object Viewer at a child project. What is their effective access there, and how could you actually reduce it?
  5. Give two ways to use a service account without downloading a key, and name the role that enables impersonation.

Answers

  1. Google account user:, Google group group:, service account serviceAccount:, domain domain: (plus the set-based allUsers/allAuthenticatedUsers and principalSet:// for federation).
  2. They are far too broad (span all services) and cannot be conditioned, so they violate least privilege. Use predefined roles by default, custom roles when none is tight enough.
  3. Policies attach to resources and are inherited downward and additively — effective access is the union of a resource’s and all its ancestors’ grants, and a child cannot reduce a parent’s grant; AWS is identity-centric where SCPs set a ceiling (only restrict) and an explicit Deny overrides any Allow.
  4. Everything Editor allows plus Object Viewer (additive union). To reduce it you must remove the folder-level Editor grant or apply a deny policy — a narrower child allow cannot subtract.
  5. Impersonation (a caller mints a short-lived token) and an attached SA (the platform supplies credentials to a VM/Cloud Run/GKE workload); the impersonation role is roles/iam.serviceAccountTokenCreator, granted on the target SA. (For external workloads, Workload Identity Federation.)

Exercise

Build a least-privilege access pattern end to end, then prove it with the tooling:

  1. Create a Cloud Identity group (or reuse one) and add yourself; from now on grant the group, not your user.
  2. Create a service account web-runtime-sa and grant it only roles/storage.objectViewer on your project.
  3. Grant your group roles/iam.serviceAccountUser on web-runtime-sa (so members could deploy a workload that runs as it).
  4. Add a self-expiring conditional grant of roles/compute.viewer to your group with request.time < timestamp("...") a week out.
  5. Run gcloud asset analyze-iam-policy --project=$PROJECT_ID --identity="group:<your-group>" and confirm the effective access matches exactly what you intended — no more, no less.
  6. Add a deny policy at the project blocking compute.googleapis.com/instances.delete for public:all with an exception for a break-glass group, then confirm with the Policy Troubleshooter that a normal member is denied.
  7. Clean up every binding, the SA, and the deny policy.

If step 5 shows precisely your three intended grants (and step 6 shows the deny taking effect over any allow), you have built and verified a real least-privilege design.

Certification mapping

Glossary

Next steps

You can now read an IAM policy, reason correctly about additive inheritance, bound grants with Conditions, override with deny policies, and — most importantly — operate service accounts without ever touching a key. Next, put it to work building a real workload in Foundational Three-Tier Web Application on Google Cloud, where a least-privilege service account is the identity your application runs as. To go deeper on the control surfaces introduced here, follow Advanced GCP IAM: Deny Policies, Conditional Bindings, and Impersonation Chains and Keyless Authentication to GCP: Workload Identity Federation for GitHub Actions and CI/CD.

gcpiamservice-accountsworkload-identity-federationleast-privilegeassociate-cloud-engineer
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading