Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GCP IAM: Roles, Permissions & Testing

Permissions vs Roles

Permission — one atomic capability, maps 1:1 to an API method:

storage.objects.get
│        │       └─ verb
service  resource

Role — a named bag of permissions. That is all it is.

You cannot assign permissions directly to a member. Only roles can be bound. If no predefined role has exactly the permissions you need → create a custom role.

# What permissions does a role contain?
gcloud iam roles describe roles/storage.objectViewer

# What roles contain a specific permission?
gcloud iam list-testable-permissions \
  //cloudresourcemanager.googleapis.com/projects/MY_PROJECT \
  | grep "storage.objects.get"

Basic vs Predefined vs Custom

BasicPredefinedCustom
Also calledPrimitive / legacyCuratedCustom
Maintained byGoogle (frozen)Google (auto-updated)You
ScopeCross-service (entire project)One service, one job functionExactly what you list
New API perms auto-added?Yes ← dangerYesNo — manual updates required
When to useThrowaway sandboxes onlyDefault choice (95% of cases)When predefined is still too broad

Basic roles — avoid in prod

roles/viewer, roles/editor, roles/owner — apply to everything in the project. editor alone carries thousands of permissions across all services.

Predefined roles — your default

Naming convention (loose, not enforced):

roles/<service>.<resource><Level>

Common suffixes and what they mean:

SuffixMeaning
ViewerRead-only
Editor / dataEditorRead + write
AdminFull control including IAM on the resource
CreatorCreate new, not manage existing
UserUse without managing
InvokerTrigger/call (Cloud Run, Cloud Functions)
JobUserSubmit jobs (BigQuery, Dataflow)

These are not a formal standard — each service team named their own roles. Some don't follow the pattern at all (roles/iam.serviceAccountTokenCreator, roles/cloudsql.client).

BigQuery needs two roles together in practice: bigquery.dataEditor (data access) + bigquery.jobUser (run queries/pay for them). Neither alone is sufficient.

Custom roles — least-privilege precision

Build by trimming a predefined role's permission list rather than from scratch:

# Dump a predefined role, trim it, create custom from it
gcloud iam roles describe roles/storage.objectAdmin --format=yaml > role.yaml
# edit role.yaml — remove unwanted permissions
gcloud iam roles create trimmedStorage --project=MY_PROJECT --file=role.yaml

Or create directly:

gcloud iam roles create dataIngester \
  --project=MY_PROJECT \
  --title="Data Ingester" \
  --permissions="storage.objects.get,storage.objects.list,bigquery.tables.create,bigquery.tables.updateData" \
  --stage="GA"

Custom roles don't auto-update — when Google adds new permissions to a service, predefined roles get them, yours does not.

Decision flow

Need to grant access?
│
├─ Throwaway sandbox / solo lab?
│     └─ Basic role is fine
│
├─ Does a predefined role match the job function?
│     └─ YES → use it  ← 95% of cases
│
└─ Predefined too broad for compliance / least-privilege?
      └─ Custom role — accept the maintenance cost

Testing IAM Without Creating Resources

IAM itself is free. Creating service accounts, bindings, and custom roles costs $0. You only pay for compute resources.

Policy Simulator (Console)

Test "if I set this policy, would X be able to do Y?" without granting anything.

console.cloud.google.com/iam-admin/simulator

Input: principal + proposed policy → output: allow/deny + which binding caused it.

test-iam-permissions (CLI)

Check what a member can do on a resource right now:

gcloud storage buckets test-iam-permissions gs://my-bucket \
  --permissions="storage.objects.get,storage.objects.list" \
  --member="user:alice@theodo.com"

gcloud projects test-iam-permissions MY_PROJECT \
  --permissions="bigquery.tables.create" \
  --member="serviceAccount:sa@MY_PROJECT.iam.gserviceaccount.com"

Read-only exploration (no side effects)

# What predefined roles exist for a service?
gcloud iam list-predefined-roles --filter="name:storage"

# What roles does a member currently have on a project?
gcloud projects get-iam-policy MY_PROJECT \
  --flatten="bindings[].members" \
  --filter="bindings.members:alice@theodo.com" \
  --format="table(bindings.role)"

# Preview a policy change before applying
gcloud projects get-iam-policy MY_PROJECT --format=yaml > policy.yaml
# edit locally, inspect, then:
gcloud projects set-iam-policy MY_PROJECT policy.yaml

Throwaway project (safest sandbox)

gcloud projects create pr01-iam-lab --organization=732586063639
# experiment freely — IAM ops are free
gcloud projects delete pr01-iam-lab   # wipes everything when done

Service Accounts — Instance Identity

Are VMs attached to a service account by default?

Yes. Every Compute Engine instance gets an identity unless you explicitly remove it — the Compute Engine default service account:

PROJECT_NUMBER-compute@developer.gserviceaccount.com

It's auto-created the moment the Compute Engine API is enabled (nobody asks for it), and new VMs attach to it automatically.

Instance identity by default?ConstructModel
GCPYes — default SA auto-attachedService accountOpt-out
AWSNo — none until attachedIAM Instance Profile (role)Opt-in
AzureNo — none until enabledManaged IdentityOpt-in

Coming from AWS/Azure this is the surprise: there, an instance has no cloud identity until you deliberately attach one. In GCP, assume an identity is always attached and make sure it's the least-privilege one you chose — not the broad default.

The legacy Editor grant

Historically, enabling Compute Engine also auto-granted the default SA roles/editor at the project level — one binding, inherited by every VM using that SA (access always follows identity + binding, never "same project = access").

roles/editor  →  bound to  →  PROJECT_NUMBER-compute@developer.gserviceaccount.com
Behavior
Org policy iam.automaticIamGrantsForDefaultServiceAccounts not enforced (older orgs)New default SAs still auto-get roles/editor
Constraint enforced (new orgs, default since May 2024)No auto-grant for newly created default SAs
Projects created before the changeStill carry the legacy Editor grant — enforcing the constraint later does not retroactively remove it

Check a project for the legacy grant:

gcloud projects get-iam-policy MY_PROJECT \
  --flatten="bindings[].members" \
  --filter="bindings.role:roles/editor AND bindings.members:*-compute@developer.gserviceaccount.com" \
  --format="table(bindings.role,bindings.members)"

The access-scope gotcha (why cloud-platform scope is dangerous with the default SA)

A VM's effective permissions = IAM role ∩ access scope (legacy scope model). Two settings, both must allow it:

VM scope settingEffect
"Allow default access" (narrow legacy scopes: read-only storage, logging/monitoring write)Even with Editor bound to the SA, the VM itself can't exercise most of it
cloud-platform scope ("Allow full access to all Cloud APIs")Scope stops limiting anything — effective access becomes the full IAM role

Dangerous combo: default SA + legacy Editor grant + cloud-platform scope → every VM in the project can act as project-wide Editor from inside the guest OS. This combo is easy to reach by accident (cloud-platform scope is the standard Terraform recommendation for a properly scoped SA — it only becomes a problem paired with an over-privileged identity).

Why a dedicated SA per workload, not the default

Default SADedicated SA
IdentityShared across every VM using itUnique per workload/tier
PermissionsWhatever's bound project-wide (often legacy Editor)Only what you explicitly grant
Blast radius if one VM is compromisedEntire projectJust what that SA can do
Audit trailEvery call logs as the same shared identityLogs show which workload did what
Enables SA-based firewalling / resource restrictionNo — tiers are indistinguishable if they share an SAYes — this is the prerequisite
Tightening perms for one workloadImpossible without affecting every VM sharing the SAChange that SA's bindings only

Concrete failure: an app VM gets RCE'd while running as the default SA with Editor + cloud-platform scope → attacker can read every bucket, tamper with Pub/Sub, create/delete Compute resources, touch BigQuery, project-wide, from one shell. A dedicated app-sa with only roles/storage.objectViewer on one bucket caps the damage to exactly that.

Pattern: grant a resource to some VMs, not all

Access to any resource (e.g. a GCS bucket) is always VM's attached SA → IAM role binding on the resource — project membership grants nothing by itself.

All VMs need access → share one SA, bind the role once:

resource "google_storage_bucket_iam_member" "vms_read" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.vm.email}"
}

Only some VMs need access → dedicated SA, attached only to those VMs:

resource "google_service_account" "bucket_reader" {
  account_id = "bucket-reader"
}

resource "google_storage_bucket_iam_member" "reader" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.bucket_reader.email}"
}

resource "google_compute_instance" "privileged" {
  # ...
  service_account {
    email  = google_service_account.bucket_reader.email
    scopes = ["cloud-platform"]   # safe here: the SA itself is least-privilege
  }
}

Every other VM, running as a different (non-bound) SA, has no access — the restriction is enforced by which SA is attached, not network placement.

Network access (firewall rules) and resource authorization (IAM bindings) are separate planes, but both key off the same SA identity — see Networking & Firewalls: AWS vs Azure vs GCP for the service-account firewalling side of this pattern.