GCP IAM Policies Explained: Bindings, Inheritance, ETags, and Safe Changes
By the end of this page you will understand what a GCP IAM policy is, what its three fields do, how access flows across the resource hierarchy, when to set access at project level versus resource level, and how to make changes without accidentally deleting existing access.
The simple version
An IAM policy is the access document attached to a Google Cloud resource. It says who gets which role on that resource.
Every project has an IAM policy. Every Cloud Storage bucket has one. Every Secret Manager secret has one. When GCP receives an API request, it looks up the relevant policies and checks whether the caller has the required permission. If yes, the request succeeds. If no policy grants that permission, GCP returns a permission denied error.
That is the whole model. The rest of this page fills in the details that matter in practice.
Think of it like an apartment lease
The lease (IAM policy) is attached to the apartment (the resource). It lists exactly who has access and what they are allowed to do. The landlord (GCP) checks the lease before letting anyone in. A role is like the type of key being granted: a master key for staff, a standard key for tenants, a mailbox key for limited access only. The policy is the document that records who holds which key.
What is an IAM policy in GCP?
An IAM policy is a structured document that controls access to a single GCP resource. Policies can be attached to:
- Organisations
- Folders
- Projects
- Cloud Storage buckets
- BigQuery datasets
- Pub/Sub topics and subscriptions
- Secret Manager secrets
- Cloud Run services
- And many other resource types
The policy is not a global list of all access across your account. It belongs to one resource and governs access to that resource only. For background on how GCP organises projects and resources, see the resource hierarchy guide. For a broader introduction to how IAM fits together, see IAM in GCP.
The three fields in every IAM policy
Every IAM policy document contains exactly three top-level fields. Understanding what each one does is the foundation of working with IAM safely.
bindings: the access grants. An array of role-to-member mappings. This is the core of the policy.etag: a concurrency control token that prevents two simultaneous updates from overwriting each other.version: the policy schema version. Version 1 is standard. Version 3 is required when the policy contains IAM Conditions.
# A typical project IAM policy
bindings:
- role: roles/storage.objectViewer
members:
- group:data-readers@example.com
- role: roles/run.invoker
members:
- serviceAccount:api-gateway@my-app-prod.iam.gserviceaccount.com
- role: roles/logging.viewer
members:
- group:platform-engineers@example.com
- user:alice@example.com
etag: BwWWja0YfJA=
version: 1Bindings
Each binding connects one role to a list of members. Members are prefixed by type to tell GCP what kind of identity it is:
user:a Google Account belonging to a personserviceAccount:a service account used by an application or workloadgroup:a Google Group containing many usersdomain:all users in a Google Workspace domainallAuthenticatedUsersany authenticated Google identity (use with caution)allUsersliterally anyone, including unauthenticated requests
Granting any role to allUsers makes that resource publicly
accessible to the entire internet, including unauthenticated requests.
This is appropriate for truly public assets like a static website bucket,
but it is a critical misconfiguration on anything that contains user data,
credentials, or internal services. Security Command Center will flag it.
One binding can have many members, but it can only reference one role. To grant two roles to the same person, you need two separate bindings.
Etag
The etag is a hash of the current policy state. When you call
get-iam-policy, the response includes the current etag. When
you call set-iam-policy, GCP checks that the etag in your
file matches the current etag on the resource.
If another change was made between your read and your write, the etags will not match and the operation fails. This prevents concurrent updates from silently overwriting each other, which is a common problem in team environments where multiple engineers might adjust IAM around the same time.
Version
Most policies use version 1. Version 3 is only required when a policy contains IAM Conditions: conditional bindings that restrict when or where access applies. If you read a version 3 policy and reapply it using an older tool that does not understand conditions, those conditions may be silently stripped. Always check the version field before writing back a policy file.
When you add a condition to a binding using gcloud, the
policy version is automatically upgraded to 3. You do not need to set
it manually.
How IAM policies work
When you make a GCP API call, IAM evaluates access like this:
- GCP identifies the resource you are trying to access.
- GCP collects all applicable IAM policies: from the resource itself, plus its parent project, any folders, and the organisation.
- GCP checks whether any binding in any of those policies grants the required permission to the caller.
- If yes, the call succeeds. If no binding matches, the call fails with a permission denied error.
Access is the union of all applicable policies. A binding at the project level is just as effective as one at the resource level. They are evaluated together, not in priority order.
You cannot revoke inherited access by changing a lower-level policy. If
a user has roles/viewer at the organisation level, they have
that access everywhere. A project-level or resource-level policy cannot
remove it. To remove broad access, you must edit the policy at the level
where it was originally granted.
Policy levels and inheritance
Policies exist at four levels of the resource hierarchy:
- Organisation: applies to every folder, project, and resource in the organisation
- Folder: applies to all projects within that folder and everything inside them
- Project: applies to all resources within the project
- Individual resource: applies only to that specific resource. Buckets, secrets, topics, and datasets all support resource-level policies.
Inheritance flows downward and is additive. If a user has
roles/viewer at the folder level, they have viewer access on
every project and resource inside that folder. Nothing at the project or
resource level can take that away.
Organisation (roles/viewer granted to alice)
βββ Folder: production
βββ Project: my-app-prod β alice has viewer here too
βββ Bucket: my-app-prod-data β and hereBuilding access analogy
IAM policies are like layered access cards in a building. The building owner gave your department a card that opens the front door (organisation level). Your floor manager gave your team a card for the floor (folder level). You have a key for your office (project level). All three cards work simultaneously, and no floor manager can revoke your building-wide access.
Running get-iam-policy on a project only shows bindings set
directly on that project. Inherited bindings from parent folders or the
organisation do not appear in the project-level output. If a user appears
to have access you cannot explain, check the folder and organisation
policies too.
Project-level vs resource-level IAM policies
One of the most practical decisions in GCP IAM is where to set a binding. You usually have two realistic options: on the project, or on the specific resource.
Project-level policies
A binding on the project applies to every resource in that project. This works well for broad, team-wide access, such as giving an engineering team read-only access to monitor everything in a project.
The trade-off is scope. A service account with
roles/storage.objectViewer at the project level can read
every bucket in the project, not just the one you intended.
Resource-level policies
A binding on an individual resource applies only to that resource. This is the safer option when access should be narrow, and it directly supports the principle of least privilege by limiting what a compromised identity can reach.
When to use each
- Use project-level when giving a team broad read-only monitoring access to a project
- Use project-level when a CI/CD service account needs to deploy across multiple services in the same project
- Use resource-level when one service account needs access to one specific bucket for an ETL job
- Use resource-level when a workload needs to access one specific secret in Secret Manager
- Use resource-level when an external identity should be allowed to invoke only one specific Cloud Run service
Set the binding at resource level first. You can always widen scope later. Narrowing scope after the fact is harder to reason about and easier to get wrong, especially once other teams start relying on the broader access.
IAM policy vs IAM role
These two terms are often confused. They are related but distinct:
- Role: a named set of permissions. For example,
roles/storage.objectViewerincludes permissions likestorage.objects.getandstorage.objects.list. Roles do not contain any information about who holds them. - Binding: a single entry inside a policy that says βthese members hold this role on this resource.β
- Policy: the full document attached to a resource, containing all bindings for that resource.
Job descriptions analogy
A role is a job description: it defines what someone in that role is allowed to do. A binding is the act of assigning that job to a specific person. The IAM policy is the full org chart for one resource, listing every person and every role they hold. Roles exist independently of people. People get access by being named in a binding inside a policy.
For a deeper look at the types of roles available, see IAM Roles Explained and Basic vs Predefined vs Custom Roles.
IAM policies and IAM Conditions
Standard IAM bindings apply unconditionally. If the member has the role on the resource, access is always active. IAM Conditions let you attach a conditional expression to a binding so that access only applies in specific circumstances.
Common uses include:
- Time-based expiry: access only during business hours or before a specific date
- Resource attribute conditions: access only to resources tagged a certain way
- Request attribute conditions: access only from a specific IP range
Using conditions requires policy version 3. When you add a condition via
gcloud, the version upgrades automatically. For the full
picture, see IAM Conditions.
How to read an IAM policy
Pull a policy with get-iam-policy like this:
# View the policy on a project
gcloud projects get-iam-policy my-app-prod
# View the 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/api-tokenWhen reading the output, look for:
- The role: check it matches your intent. Prefer predefined roles over basic roles like
roles/editor. - The members: watch for
allUsersorallAuthenticatedUsers, which grant public or near-public access. - The scope: remember that project-level output does not show inherited bindings from parent folders.
- Conditions: if a binding has a
conditionblock, check what it restricts. Conditions only work with version 3. - The etag: save this before making any changes. It protects your write from overwriting a concurrent update.
# Filter to find all roles held by one specific member
gcloud projects get-iam-policy my-app-prod \
--flatten="bindings[].members" \
--filter="bindings.members:alice@example.com" \
--format="table(bindings.role)"How to modify policies safely
There are two approaches to modifying a policy. For a complete CLI walkthrough with more examples, see Managing IAM with gcloud. For infrastructure-as-code workflows, see Managing IAM with Terraform.
Incremental binding commands (use this by default)
These commands add or remove a single binding atomically. They handle the read-modify-write cycle internally and will not touch any other bindings.
# Add one binding to a project
gcloud projects add-iam-policy-binding my-app-prod \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"
# Remove the same binding
gcloud projects remove-iam-policy-binding my-app-prod \
--member="group:platform-engineers@example.com" \
--role="roles/logging.viewer"
# Resource-level binding on a bucket (narrower than project level)
gcloud storage buckets add-iam-policy-binding gs://my-app-prod-data \
--member="serviceAccount:etl-job@my-app-prod.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"Read-modify-write (for controlled bulk changes)
When you need to make several changes at once, or manage IAM as code, export the current policy first, edit the exported file, and apply it back. The etag in the exported file is your safety net.
# Step 1: export the current policy
gcloud projects get-iam-policy my-app-prod --format=json > policy.json
# Step 2: edit policy.json (add, remove, or modify bindings)
# Step 3: apply the updated policy
gcloud projects set-iam-policy my-app-prod policy.jsonset-iam-policy replaces the full policy on the target
resource in one operation. Bindings that exist in the current policy but
are absent from your file are deleted immediately, with no warning and
no undo. This can silently remove production access for services and users
that depend on it. Always start from an exported file, review the diff
carefully, and prefer incremental binding commands for routine changes.
When to use this
Working with IAM policies directly comes up in several common scenarios:
- Giving a team viewer access to a project: set one binding at the project level so every member of a Google Group can view resources without individual setup.
- Granting a workload access to one specific resource: set a resource-level binding so a service account can only reach the one bucket or secret it needs. See service accounts for how to set these up.
- Auditing who can access a secret or service: read the policy at every relevant level to get the full picture of who has access and why.
- Preparing to use IAM Conditions: understand the current policy structure and version before adding conditional bindings to avoid unintended changes.
- Debugging a permission denied error: read the policy on the resource, then check parent levels too. See Fixing IAM Access Denied errors for a step-by-step approach.
- Moving from basic roles to predefined roles: audit current project-level policies and replace overly broad bindings with narrower, resource-specific ones.
Common mistakes
Writing a policy file from scratch and running set-iam-policy. A hand-written file will not include any existing bindings. Running
set-iam-policydeletes everything not in the file. Always export the current policy first, make your edits to that file, and review the diff before applying.Using set-iam-policy for a single binding change. For routine additions and removals, use
add-iam-policy-bindingandremove-iam-policy-binding. They are atomic and cannot accidentally remove unrelated bindings.Setting a binding at project level when resource level is enough. If a service account needs access to one bucket, setting the binding at project level gives it access to all buckets in the project. Use the most specific scope the resource supports.
Forgetting that access is inherited from parent resources. When debugging unexpected access, check the folder and organisation policies as well as the project policy. Inherited bindings do not appear in the project-level output from
get-iam-policy.Confusing a role with a policy. A role is a set of permissions. A policy is the document that grants roles to members. Granting someone a role means adding a binding inside a policy β it does not change the role itself.
Not checking the policy version before re-applying a version 3 policy. If you export a policy that contains conditions (version 3) and apply it using an older tool or workflow, conditions may be silently dropped. Verify the version field and use a compatible tool chain.
Summary
- An IAM policy is a document attached to a resource that says who holds which role. Every project, bucket, secret, and many other resources have their own policy.
- Every policy has three fields:
bindings(the access grants),etag(concurrency control), andversion(schema version). - Policies at organisation, folder, project, and resource level all apply together. Access is additive and cannot be revoked at lower levels.
- A role is a set of permissions. A binding assigns a role to members. The policy holds all bindings for one resource.
- Use
add-iam-policy-bindingandremove-iam-policy-bindingfor routine changes. They are atomic and safe. set-iam-policyreplaces the entire policy. Always export first and review the diff before applying.- Set bindings at resource level when access should be narrow. Use project level when access naturally applies across the whole project.
- Policy version 3 is required for IAM Conditions. Always check the version field before re-applying a policy file.
Frequently asked questions
What is an IAM policy in GCP?
An IAM policy is a JSON document attached to a GCP resource such as a project, bucket, or secret. It defines who has what access by listing bindings, each of which maps a role to one or more members. Policies exist at the organisation, folder, project, and individual resource level, and they all apply together.
What is the difference between an IAM policy and an IAM role?
A role is a named collection of permissions. For example, roles/storage.objectViewer grants read access to objects. A policy is the document attached to a resource that contains bindings. A binding says which members hold which role on that resource. The policy holds the bindings; the role defines the permissions those bindings grant.
Can a child resource override or block inherited IAM access?
No. IAM access is additive across the hierarchy. If a user has a role at the folder or organisation level, that access applies to every resource below it. Child resources can grant additional access but cannot revoke permissions granted by a parent. This is why it matters to set bindings at the right level.
When should I use project-level IAM instead of resource-level IAM?
Use project-level IAM when access naturally applies across the whole project, such as giving a team viewer access to monitor everything. Use resource-level IAM when you want to grant narrow access to one specific resource, such as giving a service account access to a single bucket or secret. Resource-level is safer because it limits what a compromised identity can reach.
What does the etag field do in a GCP IAM policy?
The etag is a concurrency control token. When you read a policy with get-iam-policy, the response includes an etag representing the current state. When you write back a modified policy with set-iam-policy, GCP checks that the etag in your file still matches the current etag on the resource. If someone else changed the policy between your read and write, the etags will not match and the write fails, preventing you from overwriting their changes.
Does set-iam-policy replace the full policy?
Yes. set-iam-policy replaces the entire policy on the target resource. Any bindings that exist in the current policy but are missing from your file are permanently deleted. Always export the current policy first, edit that exported file, and use add-iam-policy-binding or remove-iam-policy-binding for routine single-binding changes instead.