Part 2 of 1217 min read · 6 diagramsAI-assisted

IAM & Identity

Table of Contents#

  1. Why GCP IAM Is Structurally Different From AWS IAM
  2. The Core IAM Vocabulary
  3. Principals: Google Accounts, Groups, and Domains
  4. Service Accounts — GCP's Workload Identity
  5. Roles: Primitive, Predefined, and Custom
  6. Why Primitive Roles Are Almost Always the Wrong Choice
  7. IAM Bindings — Attaching Roles to Principals
  8. How GCP Actually Evaluates a Permission Request
  9. IAM Conditions — Context-Aware Access
  10. Deny Policies — GCP's Explicit Deny
  11. Service Account Keys — The Long-Lived Credential Problem
  12. Workload Identity Federation — Eliminating Keys Entirely
  13. Workload Identity for GKE
  14. Impersonation — Short-Lived Access Without Keys
  15. Cross-Project IAM, Worked End to End
  16. Cloud Identity and Federation With an External IdP
  17. The Principle of Least Privilege, Applied With Real GCP Tools
  18. Auditing IAM: Policy Analyzer and Recommender
  19. Part 2 CLI Cheat Sheet
  20. Common Mistakes
  21. Worked Practice Problems
  22. Summary and What's Next

Why GCP IAM Is Structurally Different From AWS IAM#

The AWS series (Part 2) established IAM as the single most important service in the entire provider — the same is true here, but worth stating a genuine structural difference immediately: AWS attaches policies to identities (or resources); GCP attaches ROLES to principals via BINDINGS on a resource's own IAM policy. The conceptual goal — govern who can do what — is identical (directly the concrete implementation of least privilege already covered in the DevSecOps series), but the mechanics are worth learning precisely on their own terms rather than assuming a one-to-one syntax mapping from AWS.


The Core IAM Vocabulary#

Diagram
TermMeaning
MemberWho's requesting access — a Google Account, Service Account, Google Group, or entire domain
RoleA named bundle of permissions (GCP IAM has no concept of a single, standalone "permission grant" — permissions only ever come bundled inside a role)
PermissionThe finest-grained unit (e.g. storage.objects.get) — never granted directly; always via a role
BindingThe actual link: THIS member has THIS role on THIS resource
PolicyThe full set of bindings attached to a resource

Why "permissions are never granted directly, only via a role" is worth stating as a precise, structural fact, a genuinely important distinction from how an AWS IAM policy statement can name individual actions directly: on GCP, even granting a single permission requires either using an existing role that happens to contain exactly that permission, or creating a CUSTOM role (covered shortly) bundling exactly the permissions needed — there's no equivalent of an inline, ad hoc AWS policy statement listing arbitrary individual actions outside of a role.


Principals: Google Accounts, Groups, and Domains#

# Grant a role to an individual Google Account
gcloud projects add-iam-policy-binding prod-payments-7f3a \
  --member="user:alice@example.com" \
  --role="roles/storage.objectViewer"

# Grant a role to an ENTIRE Google Group — the strongly
# recommended pattern for human access, directly analogous
# to the AWS series' IAM Groups discussion (Part 2)
gcloud projects add-iam-policy-binding prod-payments-7f3a \
  --member="group:data-team@example.com" \
  --role="roles/bigquery.dataViewer"

# Grant a role to an entire Workspace domain — rare, but
# worth knowing exists
gcloud projects add-iam-policy-binding prod-payments-7f3a \
  --member="domain:example.com" \
  --role="roles/viewer"

Why binding roles to GROUPS rather than individual users is the strongly recommended default, worth stating explicitly, directly reusing the AWS series' IAM Groups reasoning: adding/removing a person from the Google Group (managed centrally, likely already synced from an HR system) automatically grants/revokes their GCP access — no need to hunt down and edit individual IAM bindings scattered across every project the person needs access to.


Service Accounts — GCP's Workload Identity#

The single most important, and most genuinely distinctive, concept in this entire part. A GCP Service Account is simultaneously an IDENTITY (something you can grant roles TO, exactly like a human user) and a RESOURCE (something you can grant roles ON, controlling who can USE it) — a dual nature AWS's IAM role doesn't quite share in the same way.

Diagram
# Create a service account — directly the GCP analog of
# an AWS IAM role used by a workload (AWS series, Part 2)
gcloud iam service-accounts create my-app-sa \
  --display-name="My App Service Account" \
  --project=prod-payments-7f3a

# Grant the SERVICE ACCOUNT (as an identity) a role on a bucket
gcloud storage buckets add-iam-policy-binding gs://my-app-bucket \
  --member="serviceAccount:my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

# Grant a HUMAN (as a member) the ability to ACT AS this
# service account — controlling WHO can use its identity
gcloud iam service-accounts add-iam-policy-binding \
  my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com \
  --member="user:alice@example.com" \
  --role="roles/iam.serviceAccountUser"

Why this dual nature matters precisely, worth stating explicitly as a genuinely strong interview answer: a service account's OWN permissions (what it can access) and the permissions controlling WHO can USE that service account (roles/iam.serviceAccountUser or roles/iam.serviceAccountTokenCreator) are two SEPARATE IAM concerns — a compromised human credential with serviceAccountUser on a highly-privileged service account can effectively "become" that service account, which is exactly why controlling WHO can act as a service account deserves the same least-privilege rigor as the service account's own permissions.


Roles: Primitive, Predefined, and Custom#

Diagram
# Create a custom role with an exact, narrow permission bundle
gcloud iam roles create appReadOnlyRole \
  --project=prod-payments-7f3a \
  --title="App Read-Only Role" \
  --permissions=storage.objects.get,storage.objects.list \
  --stage=GA

Why Primitive Roles Are Almost Always the Wrong Choice#

Worth its own dedicated, explicit callout, since this is a genuinely common real mistake, directly the GCP equivalent of the AWS series' warning against AdministratorAccess.

Diagram

Why this is worth naming explicitly as one of the single most impactful least-privilege mistakes on GCP, worth stating precisely: roles/editor is often granted "to get things working quickly," and then never revisited — directly the same "temporary Resource: *, never narrowed" mistake already flagged in the AWS series (Part 2), just with GCP's own primitive-role vocabulary.


IAM Bindings — Attaching Roles to Principals#

# View the current IAM policy on a resource
gcloud projects get-iam-policy prod-payments-7f3a --format=json

# Bindings can be scoped at ANY level of the resource
# hierarchy (Part 1) — Organization, Folder, or Project,
# with the same additive inheritance already covered there
gcloud resource-manager folders add-iam-policy-binding 555566667777 \
  --member="group:platform-team@example.com" \
  --role="roles/compute.admin"

How GCP Actually Evaluates a Permission Request#

Worth memorizing precisely — a genuinely common, precise interview question.

Diagram

The single sentence worth memorizing, directly parallel to the AWS series' equivalent: "everything is implicitly denied by default; any applicable IAM binding anywhere in the resource hierarchy grants it; but a Deny Policy always wins, no matter how many Allow bindings exist elsewhere."


IAM Conditions — Context-Aware Access#

A genuinely powerful capability worth knowing precisely: an IAM binding can carry a CONDITION, restricting when it actually applies — time-based, resource-attribute-based, or the tag-based ABAC pattern already introduced in Part 1.

gcloud projects add-iam-policy-binding prod-payments-7f3a \
  --member="user:contractor@example.com" \
  --role="roles/storage.objectViewer" \
  --condition="expression=request.time < timestamp('2026-12-31T00:00:00Z'),title=contractor-access-expiry"

Why time-bound conditions are worth stating as a genuinely strong, concrete least-privilege pattern for temporary access needs, worth stating explicitly: a contractor's access AUTOMATICALLY expires at the condition's expression evaluates false — no one needs to remember to manually revoke it later, directly removing an entire class of "we forgot to remove access when the contract ended" real-world security gap.


Deny Policies — GCP's Explicit Deny#

Worth stating precisely how this differs from the AWS series' "explicit Deny in a policy statement" — GCP models this as its OWN, SEPARATE policy type entirely.

gcloud iam deny-policies create restrict-external-sharing \
  --organization=123456789012 \
  --deny-policy-file=deny-policy.yaml
# deny-policy.yaml
rules:
  - denyRule:
      deniedPrincipals:
        - "principalSet://goog/public:all"
      deniedPermissions:
        - "storage.googleapis.com/objects.get"
      denialCondition:
        expression: "!resource.matchTag('environment', 'public')"

Why Deny Policies being a SEPARATE resource type (not a clause inside an ordinary IAM binding, the way AWS expresses an explicit Deny as just another policy statement) is worth stating precisely: they're specifically designed to express organization-wide, override-everything guardrails — a Deny Policy at the Organization level wins regardless of how many permissive IAM bindings exist at any lower level, directly the same "guardrail beyond any individual grant" idea already established for SCPs and Organization Policy constraints, now expressed as an explicit, dedicated Deny mechanism.


Service Account Keys — The Long-Lived Credential Problem#

Directly the GCP-specific version of the AWS series' warning against long-lived IAM user access keys (AWS series, Part 2) — genuinely one of the most important security topics in this entire series.

# Creating a key is POSSIBLE, but should be treated as a
# last resort, not a default
gcloud iam service-accounts keys create key.json \
  --iam-account=my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com

Why a downloaded service account key file is worth treating with the exact same suspicion as an AWS IAM user access key, worth stating precisely: it's a standing, long-lived credential that can leak, get committed to a repository (directly connects to secret-scanning, DevSecOps series Part 4), and never expires on its own unless someone manually rotates or revokes it — modern GCP guidance, exactly like modern AWS guidance, strongly favors eliminating keys entirely wherever possible, via the mechanisms covered next.


Workload Identity Federation — Eliminating Keys Entirely#

The direct GCP analog of the AWS series' OIDC-federation-for-CI pattern — letting an EXTERNAL identity provider (GitHub Actions, AWS, Azure AD, or any OIDC-compliant IdP) directly assume GCP identity, with ZERO downloaded key file ever needed.

Diagram
gcloud iam workload-identity-pools create github-pool --location=global
gcloud iam workload-identity-pools providers create-oidc github-provider \
  --workload-identity-pool=github-pool --location=global \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"

gcloud iam service-accounts add-iam-policy-binding \
  my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com \
  --member="principalSet://iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/github-pool/attribute.repository/org/repo" \
  --role="roles/iam.workloadIdentityUser"

Why this is worth stating as achieving the EXACT SAME security outcome already established for AWS's OIDC-based CI federation (AWS series, Part 2), worth stating explicitly: a GitHub Actions workflow never needs a downloaded, long-lived service account key stored as a GitHub secret at all — it authenticates using its own short-lived, GitHub-issued OIDC token, which GCP verifies and exchanges for temporary, scoped access, eliminating an entire class of standing-credential leak risk from the CI/CD pipeline.


Workload Identity for GKE#

The GCP-native, GKE-specific application of the same Workload Identity Federation mechanism — directly, precisely the GCP analog of IRSA already covered for EKS in the Kubernetes Deep Dive series (Part 5) and the AWS series (Part 2).

# Enable Workload Identity on a GKE cluster
gcloud container clusters update my-cluster \
  --workload-pool=prod-payments-7f3a.svc.id.goog

# Bind a Kubernetes ServiceAccount to a GCP service account
gcloud iam service-accounts add-iam-policy-binding \
  my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com \
  --member="serviceAccount:prod-payments-7f3a.svc.id.goog[my-namespace/my-ksa]" \
  --role="roles/iam.workloadIdentityUser"

Why this solves the exact same problem IRSA solves on EKS, worth stating explicitly, directly closing the loop with the Kubernetes Deep Dive series: without it, every pod on a GKE node would effectively share whatever broad permissions were attached to the underlying node's own service account — Workload Identity lets each Kubernetes ServiceAccount assume its OWN, narrowly-scoped GCP service account identity, achieving true per-workload least privilege even though many unrelated pods share the same underlying node.


Impersonation — Short-Lived Access Without Keys#

A genuinely useful, everyday capability worth knowing precisely: a human (or another service account) with the right permission can TEMPORARILY act as a service account, without ever downloading its key.

# Impersonate a service account for a single command,
# getting short-lived credentials for that one call only
gcloud storage ls gs://my-app-bucket \
  --impersonate-service-account=my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com

# Or set it for the whole session
gcloud config set auth/impersonate_service_account my-app-sa@prod-payments-7f3a.iam.gserviceaccount.com

Why this is worth using as the default way for a human to test "what can this service account actually do," worth stating explicitly, directly reusing the AWS series' AssumeRole pattern: it produces the exact same short-lived, automatically-expiring credential model already established as best practice — with the added benefit that every impersonated action is clearly, separately audit-logged as an impersonation event, not conflated with the service account's own automated activity.


Cross-Project IAM, Worked End to End#

Directly the GCP analog of the AWS series' cross-account access pattern (Part 2) — a service account in one project can be granted access to resources in a COMPLETELY DIFFERENT project.

# A CI/CD service account living in a dedicated "tooling"
# project, granted deployment permission on the PRODUCTION
# project — directly the same cross-project isolation
# pattern already covered for AWS cross-account roles
gcloud projects add-iam-policy-binding prod-payments-7f3a \
  --member="serviceAccount:deployer-sa@tooling-project-1a2b.iam.gserviceaccount.com" \
  --role="roles/run.developer"

Why this pattern matters just as much here as it did in the AWS series, worth stating explicitly: it means the CI/CD tooling project itself never needs standing, broad production access — a compromised pipeline can only reach production via this SPECIFIC, narrowly-scoped cross-project binding, not through any implicit, broader trust.


Cloud Identity and Federation With an External IdP#

Directly the GCP analog of the AWS series' IAM Identity Center federation discussion (AWS series, Part 2) — Cloud Identity (or a full Google Workspace subscription) is the identity foundation every Google Account, Group, and Organization ultimately rests on, and it can itself be federated from an external corporate IdP (Okta, Azure AD) via SAML/OIDC.

Why centralizing identity through the corporate IdP matters just as much here, worth restating precisely: exactly one place an employee's access gets revoked when they leave — the corporate IdP — rather than needing to separately track down Google Accounts scattered across the organization's Cloud Identity directory.


The Principle of Least Privilege, Applied With Real GCP Tools#

# Policy Analyzer — see WHO has WHAT access, and WHY
# (including inherited bindings from the resource hierarchy)
gcloud asset analyze-iam-policy \
  --organization=123456789012 \
  --identity="user:alice@example.com"

# IAM Recommender — GENERATES a least-privilege
# recommendation based on ACTUAL usage, directly the
# GCP analog of AWS Access Analyzer's policy generation
# (AWS series, Part 2)
gcloud recommender recommendations list \
  --project=prod-payments-7f3a \
  --recommender=google.iam.policy.Recommender \
  --location=global

Auditing IAM: Policy Analyzer and Recommender#

Why the IAM Recommender's usage-based approach matters, worth stating explicitly, directly the same reasoning already established for AWS Access Analyzer's policy generation: it examines what a principal ACTUALLY used over a real historical window, then recommends narrowing an overly broad role (e.g. suggesting roles/storage.objectViewer in place of roles/editor, if that's genuinely all the account ever used) — reverse-engineering least privilege from real behavior, rather than requiring someone to guess correctly upfront.


Part 2 CLI Cheat Sheet#

AreaCommandPurpose
Identitygcloud auth listConfirm current identity
Bindingsgcloud projects add-iam-policy-bindingGrant a role to a member on a project
Service accountsgcloud iam service-accounts createCreate a service account
Service accountsgcloud iam service-accounts add-iam-policy-bindingControl who can act as a service account
Custom rolesgcloud iam roles createDefine a precise, narrow permission bundle
Conditions--condition= flag on any binding commandAdd time/attribute-based scoping
Deny policiesgcloud iam deny-policies createCreate an override-everything guardrail
Workload Identitygcloud iam workload-identity-pools createSet up keyless CI/CD federation
Impersonation--impersonate-service-account= flagTemporarily act as a service account
Auditinggcloud asset analyze-iam-policySee who has what access, and why
Auditinggcloud recommender recommendations listGet usage-based least-privilege suggestions

Common Mistakes#

MistakeWhy It's WrongFix
Granting roles/editor "to get things working"Vastly exceeds what most workloads actually need — the GCP equivalent of AWS's AdministratorAccess mistakeUse predefined or custom roles scoped to the exact permissions needed
Downloading and distributing service account keys for CI/CDA standing, long-lived credential that can leak and never expires on its ownUse Workload Identity Federation for keyless, short-lived authentication
Confusing a service account's OWN permissions with who can ACT AS itBoth are separate, equally important IAM concerns — an over-permissive serviceAccountUser grant can let a compromised human credential "become" a highly-privileged service accountApply least privilege to BOTH the service account's own roles and who can impersonate it
Assuming a lower folder/project can restrict an inherited IAM bindingIAM bindings are strictly additive down the hierarchy — no restrictive override exists at a lower levelGrant broad roles only at the narrowest level genuinely appropriate
Sharing broad EKS/GKE node-level service account permissions across unrelated workloadsEvery pod on the node gets the same broad permissions, violating least privilegeUse Workload Identity to scope permissions per Kubernetes ServiceAccount

Worked Practice Problems#

Problem 1: A security review finds a GitHub Actions CI/CD pipeline authenticating to GCP using a downloaded service account key stored as a GitHub repository secret. The security team flags this as a real risk even though the repository is private. What's the concern, and what's the recommended fix?

Answer: A downloaded service account key is a standing, long-lived credential — even in a "private" repository, it remains a genuine leak risk (through a compromised CI runner, a misconfigured repository permission, a supply-chain attack on a GitHub Action dependency, or simple human error), and unlike a short-lived token, it doesn't expire on its own, meaning a leak discovered late could have been exploitable for a long time already. The recommended fix is Workload Identity Federation, configured to trust GitHub Actions' own OIDC token issuer directly — the pipeline authenticates using GitHub's own short-lived, workflow-scoped token, which GCP exchanges for temporary credentials, with no downloaded key file (and therefore no standing secret to leak) ever involved at all.

Problem 2: A team grants a data analyst's Google Account roles/editor on a project so they can query BigQuery tables, reasoning "it's easier than figuring out the exact permission needed." Three months later, a security audit finds the same analyst account was used to accidentally delete a Compute Engine instance during an unrelated cleanup task. What's the underlying cause, and what should have been granted instead?

Answer: The underlying cause is that roles/editor grants broad, near-unrestricted modify access across nearly every resource type in the project — including Compute Engine, which the analyst never actually needed for their BigQuery-only work — meaning the accidental deletion was entirely possible because of over-permissioning, not a targeted attack or a BigQuery-specific mistake. The correct grant from the start would have been a predefined role scoped specifically to BigQuery read access (e.g. roles/bigquery.dataViewer and roles/bigquery.jobUser), which would have made the accidental Compute Engine deletion structurally impossible — the analyst's account simply wouldn't have had the permission to affect that resource type at all, regardless of intent.

Problem 3: A platform team wants a service account used by a scheduled batch job to only be usable during its actual scheduled execution window (2am-4am daily), and to be structurally denied from acting outside that window even if the underlying credentials were somehow compromised and used elsewhere. What GCP IAM feature achieves this, and how?

Answer: An IAM Condition on the relevant binding(s), expressing a time-of-day restriction using the request.time context variable (a recurring daily window requires a slightly more involved CEL expression than the simple absolute-date example shown in this part, but the same condition mechanism applies). This directly limits WHEN the granted role's permissions actually take effect — even if the service account's credentials were compromised and an attacker attempted to use them outside the 2am-4am window, the IAM Condition itself would deny the request, regardless of whether the underlying credential is technically valid, providing a genuine, structural time-based blast-radius limit beyond what credential security alone can guarantee.


Summary and What's Next#

  • GCP IAM attaches roles (bundles of permissions) to members via bindings on a resource — permissions are never granted standalone, only through a role.
  • Service accounts have a genuinely distinctive dual nature: an identity that can be GRANTED roles, and a resource that others can be granted roles ON (controlling who can act as it) — both deserve equal least-privilege scrutiny.
  • Primitive roles (Owner/Editor/Viewer) are almost always too broad for production use — predefined or custom roles are the correct default.
  • Evaluation logic: a Deny Policy always wins; otherwise any applicable IAM binding anywhere in the resource hierarchy grants access; everything else is implicitly denied.
  • IAM Conditions enable time-bound and attribute-based access, directly supporting genuinely strong least-privilege patterns like automatic contractor-access expiry.
  • Workload Identity Federation (for external CI/CD) and Workload Identity for GKE both eliminate downloaded service account keys entirely — the GCP-native equivalents of AWS's OIDC federation and IRSA.
  • Impersonation provides short-lived, cleanly-audited access without ever downloading a key — the default way a human should interact with a service account's identity.

Continue to Part 3 (03-compute-engine-and-autoscaling.md) to see how IAM service account identities attach to the actual compute layer — Compute Engine instances, instance templates, and Managed Instance Groups.