Managing IAM in GCP with gcloud: Grant, Remove, and Audit Access Safely
Every IAM change made in the Console is a click-and-save with no record in
your team’s documentation. The same change made via gcloud can
live in a runbook, be reviewed in a pull request, scripted for onboarding,
and reproduced exactly the same way in every environment. This guide covers
the correct commands, the right workflow, and the mistakes to avoid.
What you will learn
By the end of this page you will be able to:
- Read and understand an IAM policy from the command line
- Grant and remove roles at both project and resource scope
- Understand when to prefer resource-level bindings over project-level
- Audit who has access to what using
get-iam-policyqueries - Avoid the most dangerous IAM command for routine changes:
set-iam-policy - Script repeatable access changes for onboarding, offboarding, and automation
- Understand the two distinct IAM roles that service accounts play
If you are new to IAM concepts, start with IAM in GCP and IAM Roles Explained before continuing here.
What it means to manage IAM with gcloud
IAM in GCP controls who can do what on which resource. Every permission
flows through a binding. A binding says: this principal holds this role on
this resource. The Console lets you manage bindings visually.
gcloud lets you do the same thing from a terminal.
Every employee has a badge. That badge unlocks certain doors. IAM is the
system that decides which badge opens which door. gcloud is
the terminal you use to program the access control panel. Adding a
binding is like granting a badge access to a new door.
Removing a binding takes that door off the badge. Using
set-iam-policy is like replacing the entire access database
from scratch — dangerous if you do not have every current entry captured
in your file.
The reason teams use the CLI alongside the Console comes down to three properties:
- Repeatability. A script you run today can be run again tomorrow, in another project, or by another team member, and will produce the same result.
- Reviewability. CLI commands can live in a runbook or pull request. Anyone can read exactly what changed and why. Console clicks leave no written record.
- Scriptability. You can loop over a list of users, projects, or resources and apply access changes programmatically. You cannot do that in the Console.
The right workflow for every IAM change
Before touching any IAM policy, follow this sequence:
- Read first. Run
get-iam-policybefore making any change. Know what is already in place. - Make the smallest possible change. Use
add-iam-policy-bindingorremove-iam-policy-binding, notset-iam-policy. One command, one binding. - Prefer narrow scope. If the service supports resource-level bindings (buckets, secrets, Pub/Sub topics), bind there rather than at project level.
- Verify the result. Read the policy back after every change. Confirm the binding you intended is present and nothing else changed.
- Prefer groups over users. Bind a Google Group rather than an individual user account. Update group membership centrally instead of hunting down bindings across projects.
Read first. Change second. Verify third. This habit prevents the class of IAM mistakes that are hardest to diagnose: silent overwrites, missing bindings, and over-broad access that accumulates over time.
Common gcloud IAM commands
These are the commands you will use for almost every IAM task. Each one is shown with a working example and a short explanation of what it does.
get-iam-policy: read the current policy
Use this before and after every change. It returns the full list of bindings set directly on the resource. For projects, folders, organisations, and most individual resources, the pattern is the same.
# View the full IAM policy on a project
gcloud projects get-iam-policy PROJECT_ID
# Output as JSON for scripting or export
gcloud projects get-iam-policy PROJECT_ID --format=json
# Find all roles held by a specific member
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:alice@example.com" \
--format="table(bindings.role)"
# Policy on a specific resource
gcloud storage buckets get-iam-policy gs://BUCKET_NAME
gcloud secrets get-iam-policy projects/PROJECT_ID/secrets/SECRET_NAMEadd-iam-policy-binding: grant a role safely
This is the safe way to grant access. It adds exactly one binding without affecting any other. Internally it performs a read-modify-write on your behalf, so you never have to handle the full policy JSON yourself.
# Grant a role at project level
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"
# Grant at resource scope (preferred when available)
gcloud storage buckets add-iam-policy-binding gs://BUCKET_NAME \
--member="serviceAccount:etl-job@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
# Grant with an expiry condition (auto-revokes after the date)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="user:contractor@external.com" \
--role="roles/logging.viewer" \
--condition="expression=request.time < timestamp('2026-12-01T00:00:00Z'),\
title=expires-december-2026,description=Temporary audit access"The —role flag takes a fully qualified role name such as
roles/logging.viewer or a custom role path like
projects/PROJECT_ID/roles/ROLE_ID. See
IAM Roles Explained for a
guide to role naming. For time-limited access using conditions, see
IAM Conditions.
remove-iam-policy-binding: revoke a role safely
Removes exactly one binding. The same safety guarantee applies: all other bindings are left untouched.
# Remove a project-level binding
gcloud projects remove-iam-policy-binding PROJECT_ID \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"
# Remove a resource-level binding
gcloud storage buckets remove-iam-policy-binding gs://BUCKET_NAME \
--member="serviceAccount:etl-job@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"Service account IAM policy binding
Service accounts have their own add-iam-policy-binding
command because they are both principals (identities that act on resources)
and resources (objects that other principals can manage or impersonate).
The full explanation is in the
service accounts section below.
# Grant a user permission to impersonate a service account
gcloud iam service-accounts add-iam-policy-binding \
SA_EMAIL \
--member="user:alice@example.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=PROJECT_ID
# Remove impersonation permission
gcloud iam service-accounts remove-iam-policy-binding \
SA_EMAIL \
--member="user:alice@example.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=PROJECT_IDReading policies before making changes
The first rule of IAM changes: read the current state before modifying it. You need to know what is already in place so you can verify that your change had exactly the intended effect and nothing else was affected.
# View the full IAM policy on a project
gcloud projects get-iam-policy my-app-prod
# Output as JSON for scripting
gcloud projects get-iam-policy my-app-prod --format=json
# Find all bindings for a specific member
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--filter="bindings.members:alice@example.com" \
--format="table(bindings.role)"
# View policy on a specific resource
gcloud storage buckets get-iam-policy gs://my-app-prod-data
gcloud secrets get-iam-policy projects/my-app-prod/secrets/db-passwordProject-level policy output shows only bindings set directly on the project. Inherited bindings from folder or organisation level do not appear. If a user has unexpected access, also check parent-level policies. The Console’s Policy Analyzer can show inherited access for a specific principal across the full resource hierarchy.
Granting and removing access safely
Use add-iam-policy-binding and
remove-iam-policy-binding for all routine access changes.
These commands change exactly one binding and leave everything else alone.
Project-level bindings
A project-level binding applies to everything inside that project. A
roles/storage.objectViewer grant at project level gives read
access to every bucket in the project. Use this only when broad access
is genuinely required.
# Grant read access to logs across the whole project
gcloud projects add-iam-policy-binding my-app-prod \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"
# Revoke when no longer needed
gcloud projects remove-iam-policy-binding my-app-prod \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"Resource-level bindings
Resource-level bindings apply to a single resource. This is almost always
the better choice. A roles/storage.objectCreator grant on one
bucket means the principal can only write to that bucket, not to any other
bucket, secret, or resource in the project. Prefer the narrowest scope
the service supports.
# Grant write access to one specific bucket
gcloud storage buckets add-iam-policy-binding gs://my-app-prod-reports \
--member="serviceAccount:etl-job@my-app-prod.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
# Grant access to one secret only
gcloud secrets add-iam-policy-binding db-password \
--project=my-app-prod \
--member="serviceAccount:api-server@my-app-prod.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"Project-level access is like giving someone a master key to the whole building when they only need to access one room. Resource-level bindings give access to exactly the resource that needs it. When a role is available at the resource level, use it there.
Temporary access with IAM Conditions
Use —condition to add a time-based expiry to any binding.
The binding is automatically denied after the date you specify, with no
manual cleanup required. This is useful for contractor access, incident
response, or any situation where access should not be permanent. See
IAM Conditions for the
full syntax and other condition types.
gcloud projects add-iam-policy-binding my-app-prod \
--member="user:contractor@external.com" \
--role="roles/logging.viewer" \
--condition="expression=request.time < timestamp('2026-12-01T00:00:00Z'),\
title=expires-december-2026,description=Temporary audit access"When to use gcloud for IAM changes
Not every IAM change needs to go through gcloud, but these
are the situations where it clearly earns its place:
- One-off access changes where you want a clear record of exactly what you ran and why.
- Debugging permission issues: quickly read the policy on a resource to see who has access. Pair this with Permission Denied Errors when access is unexpectedly blocked.
- Onboarding and offboarding: a short script that grants or revokes a standard set of roles is faster and less error-prone than clicking through the Console for every team change.
- Scripting repeatable access changes: loop over a list of service accounts or environments and apply bindings consistently.
- Environments where Console access is restricted: many production environments restrict Console access for compliance reasons. CLI and automation are the only paths available.
- Periodic access reviews: query and filter bindings directly from the terminal to verify who holds what at a point in time.
gcloud vs Console vs Terraform
All three tools can manage GCP IAM bindings. The right choice depends on what you are trying to accomplish:
| Tool | Best for | Trade-offs |
|---|---|---|
| gcloud | Operational changes, debugging, scripting, one-off grants | Fast and precise; not tracked in version control by default |
| Console | Exploring the current state, ad hoc investigation | Visual and easy; no auditability, hard to repeat |
| Terraform | Long-lived access, version-controlled policies, team review flows | Slower to iterate; requires state management |
A common pattern in mature teams: use gcloud for day-to-day
operational access and Terraform for baseline access that defines what
every team or service account should always have.
See Managing IAM
with Terraform for a full guide, including how
google_project_iam_member and
google_project_iam_binding differ and when to use each.
Never use set-iam-policy for routine changes
gcloud projects set-iam-policy replaces the complete IAM
policy document with the file you provide. Any binding that exists on the
project but is absent from your file is silently deleted.
This includes bindings you did not know existed: service agent bindings,
bindings added by other team members, and role grants set during project
setup.
For routine IAM changes, always use add-iam-policy-binding
or remove-iam-policy-binding. These commands change exactly
one binding and leave the rest of the policy untouched.
Using set-iam-policy to add one binding is like replacing
your entire company’s employee directory because you wanted to add one
new person. If you forget to copy over anyone’s existing entry, they
lose access immediately. Use the add/remove binding commands instead.
They are surgical. set-iam-policy is a sledgehammer.
set-iam-policy is not a broken command. It is the right tool
for bulk, controlled operations where you explicitly intend to define the
full desired state of a policy. The safe way to use it is to start from a
freshly exported policy, make your edits, and then apply:
# SAFE: export first, edit the file, then apply
gcloud projects get-iam-policy my-app-prod --format=json > policy.json
# Edit policy.json carefully to add or remove the bindings you intend
gcloud projects set-iam-policy my-app-prod policy.json
# RISKY: building a policy.json from scratch and applying it
# Any binding you omit will be deleted immediately and silentlyVerifying changes after applying them
After every IAM change, read the policy back. Do not assume the command worked as intended. This 30-second habit prevents a class of IAM mistakes that are hard to diagnose after the fact.
Verification checklist:
- Run
get-iam-policyand confirm your new binding is present - Confirm no other bindings changed unexpectedly
- For resource-level changes, query the resource policy directly, not just the project policy
- If the access is needed urgently, test it by running a command as the principal
# Confirm the binding you just added is present
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--filter="bindings.members:group:platform-engineers@example.com" \
--format="table(bindings.role)"
# For resource-level bindings, query the resource directly
gcloud storage buckets get-iam-policy gs://my-app-prod-reports \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:etl-job@my-app-prod.iam.gserviceaccount.com"Service accounts have two IAM angles
Service accounts appear in two completely different contexts in IAM, and
both are managed with gcloud. Understanding the difference
prevents a common source of confusion.
Think of a service account like an employee who also has a locker. You can give the employee access to resources (they are acting as a principal). Or you can give someone else the key to their locker to act on their behalf (they are acting as a resource). These are two different operations on two different targets.
Service account as a principal
When a service account needs to access a resource, such as reading from a Cloud Storage bucket or accessing a secret, you bind a role to the service account on that resource. The service account is the principal being granted access.
# The service account is being granted access to a resource
gcloud storage buckets add-iam-policy-binding gs://my-app-prod-data \
--member="serviceAccount:api-server@my-app-prod.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"Service account as a resource
When a human user or another service account needs to act as a service account, you bind a role on the service account itself. Here the service account is the resource, not the principal. You are granting someone the ability to use that service account’s identity.
# A user is being granted permission to impersonate the service account
# The service account is the resource here
gcloud iam service-accounts add-iam-policy-binding \
ci-deployer@my-app-prod.iam.gserviceaccount.com \
--member="user:alice@example.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=my-app-prodFor a full explanation of impersonation, how it works, and how to use it securely without key files, see Service Account Impersonation. For background on service accounts themselves, see Service Accounts.
Auditing access with gcloud
These queries answer the most common questions during access reviews,
all from the CLI. Combine get-iam-policy with
—flatten, —filter, and —format
to extract exactly the view you need.
# All roles and members in the project as a flat list
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--format="table(bindings.role,bindings.members)"
# Every role held by a specific group
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--filter="bindings.members:group:on-call-engineers@example.com" \
--format="table(bindings.role)"
# All direct user: bindings (should be rare in a well-managed project)
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--filter="bindings.members:user:" \
--format="table(bindings.role,bindings.members)"Direct user bindings should be rare. Most access should flow through Google
Groups so membership can be managed centrally. Any user:
binding is a candidate for migration to a group or removal.
For longer-term audit workflows, including who changed what and when, see Cloud Audit Logs. For debugging active permission failures, see Fixing IAM AccessDenied Errors.
Scripting IAM changes for onboarding and offboarding
Scripted IAM changes are repeatable, reviewable, and auditable. Treat IAM scripts the same as infrastructure code: test in a non-production project first and store them in version control.
# Onboarding: grant a new engineer read access to logs and monitoring
NEW_USER="new.hire@example.com"
PROJECT="my-app-prod"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$NEW_USER" \
--role="roles/logging.viewer"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="user:$NEW_USER" \
--role="roles/monitoring.viewer"
echo "Verifying bindings for $NEW_USER:"
gcloud projects get-iam-policy "$PROJECT" \
--flatten="bindings[].members" \
--filter="bindings.members:$NEW_USER" \
--format="table(bindings.role)"If you manage IAM across more than a handful of projects, consider
Terraform with
google_project_iam_member. Terraform state tracks which
bindings you own, and terraform plan shows exactly what will
change before you apply.
Common mistakes
Using
set-iam-policyfor a single binding change. If the JSON file you supply is missing any existing bindings, those bindings are removed immediately and silently. Useadd-iam-policy-bindingfor routine changes. It is safer, takes the same arguments, and never touches bindings you did not specify.Granting project-level access when resource-level is available. A project-level
storage.objectViewergrant gives read access to every bucket in the project. A bucket-level grant limits access to one bucket. Always prefer the most specific scope the service supports. See Principle of Least Privilege for why this matters in practice.Binding individual user accounts instead of groups. When you bind a user directly and they leave or change roles, someone must find and remove every binding they hold across every project. Google Groups centralise this: update group membership once and all downstream access updates automatically.
Not verifying the change after applying it. Read the policy back after every modification. Confirm your binding is present and nothing else changed. This takes 30 seconds and prevents mistakes that are difficult to diagnose later.
Confusing service account resource permissions with impersonation permissions. Granting a service account access to a resource is different from granting a user the ability to impersonate that service account. These are two separate operations on two separate targets, managed by two separate
add-iam-policy-bindingcommands. See Service Account Impersonation for the full distinction.Escalating roles as a quick fix when access is denied. When a permission error appears, the tempting fix is to escalate the role until it works. Start with the narrowest role that could cover the required action and only escalate if it is genuinely insufficient. See Basic vs Predefined Roles to understand which roles are almost always too broad for production use.
Summary
- Use
add-iam-policy-bindingandremove-iam-policy-bindingfor every routine change. They are atomic and do not touch unrelated bindings. - Read the policy before you change it, and read it back after to verify: read first, change second, verify third.
set-iam-policyreplaces the entire policy. Only use it when you explicitly intend that, starting from a fresh export.- Prefer resource-level bindings (bucket, secret, topic) over project-level wherever the service supports them.
- Bind Google Groups, not individual users, so access can be managed centrally and offboarding is reliable.
- Service accounts are both principals (acting on resources) and resources (others acting as them). The two operations use different commands on different targets.
- For long-lived, version-controlled IAM management across many projects, Terraform is the stronger long-term tool.
Frequently asked questions
How do I grant an IAM role using gcloud?
Use gcloud projects add-iam-policy-binding PROJECT_ID --member=user:name@example.com --role=roles/ROLE to grant at project level. For resource-level bindings, use the equivalent command for that resource — for example, gcloud storage buckets add-iam-policy-binding gs://BUCKET --member=... --role=... The add-iam-policy-binding commands are atomic and will not touch other bindings.
What is the difference between add-iam-policy-binding and set-iam-policy?
add-iam-policy-binding performs a safe read-modify-write to add a single binding without affecting any others. set-iam-policy replaces the entire policy with the file you provide — if the file is missing any existing bindings, they are deleted. Use add-iam-policy-binding for routine changes. Use set-iam-policy only for controlled bulk changes that start from a freshly exported policy.
How do I list all IAM bindings for a project using gcloud?
Run gcloud projects get-iam-policy PROJECT_ID to get the full policy. Add --flatten=bindings[].members --format=table to see a flat list of all member-role pairs. Use --filter=bindings.members:EMAIL to find all bindings for a specific identity. This only shows project-level bindings — inherited bindings from folder or organisation level are not included.
Can I grant access at bucket level instead of project level?
Yes. Use gcloud storage buckets add-iam-policy-binding gs://BUCKET_NAME --member=... --role=... to bind a role directly on a specific bucket. This is almost always preferable to a project-level grant because it limits the blast radius to that one bucket rather than every bucket in the project.
How do I let a user impersonate a service account using gcloud?
Use gcloud iam service-accounts add-iam-policy-binding SA_EMAIL --member=user:USER_EMAIL --role=roles/iam.serviceAccountTokenCreator --project=PROJECT_ID. This grants the user the ability to generate tokens as the service account without creating a key file. See the service account impersonation guide for full details and when to use this pattern.
How do I find all direct user bindings in a project?
Run: gcloud projects get-iam-policy PROJECT_ID --flatten="bindings[].members" --filter="bindings.members:user:" --format="table(bindings.role,bindings.members)" — This returns all direct user account bindings. In well-managed projects, direct user bindings should be rare; most access should flow through Google Groups for scalability and easier offboarding.