Skip to main content
Back
DateRead12 min

GCP IAM Conditions Explained: Syntax, CEL Examples, and Use Cases

Most misconfigured IAM Conditions fail without surfacing an error. You add a conditional binding for a principal who already holds the same role unconditionally, and nothing changes. The condition is never evaluated because the unconditional binding grants the role regardless. That silent non-effect is the most common way conditional access controls fail in production. Understanding how IAM Conditions actually evaluate, and where they can and can't restrict access, is where the work starts.

Key Takeaways

  • GCP IAM Conditions attach attribute-based logic to role bindings using Common Expression Language (CEL), making access conditional on resource type, resource name, time, tags, or request attributes.
  • A conditional binding does not override or narrow an unconditional binding for the same principal and role. If a principal already has the role without conditions, adding a conditioned version of that same role has no effect.
  • Conditions cannot be applied to legacy basic roles (Owner, Editor, Viewer) or to allUsers and allAuthenticatedUsers principals.
  • Conditions in deny policy rules support a much smaller attribute set than conditions in allow policy bindings: deny rules only recognize resource tag attributes.

What GCP IAM Conditions Are

A GCP IAM Condition is an expression-based gate attached to a role binding. Where a standard binding grants a principal a role unconditionally, a conditional binding grants the role only when a boolean expression written in Common Expression Language (CEL) evaluates to true at request time. If the expression evaluates to false, the binding is ignored as if it does not exist.

Conditions can appear in three places: inside role bindings in allow policies, inside deny rules in deny policies, and in policy bindings for principal access boundary policies. The mechanism is the same in each case, but the condition attributes available differ by policy type. Allow policy conditions have access to the widest set of attributes. Deny rule conditions are restricted to resource tag functions only. Role bindings created through Privileged Access Manager entitlements can carry conditions as well; those are allow policy bindings.

IAM Conditions and GCP Organization Policies operate at different layers and answer different questions. Conditions scope who can access a resource, based on attributes of the request or resource. Org Policies define which configurations are permitted to exist at all. For a full breakdown of how they relate, see the GCP Org Policy vs. IAM comparison.

Where IAM Conditions Sit in the GCP Policy Hierarchy

GCP IAM policies follow a resource hierarchy: organization > folder > project > resource. Role bindings, including conditional bindings, can be applied at any level. A binding at the organization level propagates down to all resources below it. A binding at the project level applies to all resources in that project.

Conditions inherit the same way. A conditional binding placed at the project level applies to all resources in that project, with the CEL expression evaluated in the context of each specific access request. You scope access to a narrower set of resources by adding resource name or resource type constraints to the condition expression itself, not by placing the binding at a lower level.

One important consequence: if you need to ensure a principal can only access a specific Cloud Storage bucket, you apply the binding at the project level and constrain it with a resource.name.startsWith() condition. The bucket does not need its own binding for this to work. Uniform bucket-level access is required only to put a condition in the bucket's own allow policy; Google's documented alternative, when uniform bucket-level access can't be enabled, is exactly this project-level binding, which the bucket inherits. Be aware that with uniform bucket-level access disabled, object ACLs can still grant access the condition was meant to scope out.

CEL Syntax: The Building Blocks

Every IAM Condition expression lives in the condition object of a role binding:

json
"bindings": [
  {
    "role": "roles/storage.objectViewer",
    "members": ["user:analyst@example.com"],
    "condition": {
      "title": "Prod-data-read-only-until-EXPIRY_DATE",
      "description": "Scoped to prod bucket, expires EXPIRY_DATE",
      "expression": "resource.name.startsWith('projects/_/buckets/prod-data/') && request.time < timestamp('EXPIRY_DATE')"
    }
  }
]

Replace EXPIRY_DATE with an RFC 3339 timestamp (for example, 2027-03-01T00:00:00Z). The title field is required. Description is optional. Both appear in audit logs, so meaningful values matter for operational visibility.

CEL in IAM Conditions supports four constructs:

Variables are attributes populated at request time. The most commonly used are:

  • resource.type: the resource's type identifier string (for example, compute.googleapis.com/Instance)
  • resource.name: the full resource name (for example, projects/_/buckets/my-bucket)
  • resource.service: the Google Cloud service (for example, storage.googleapis.com)
  • request.time: a Timestamp evaluated at request time
  • destination.ip and destination.port: available for IAP TCP tunneling conditions only
  • request.host and request.path: available for Identity-Aware Proxy and Cloud Run conditions only
  • principal.type and principal.subject: available in principal access boundary policy bindings only
  • request.auth.access_levels: the Access Context Manager access levels a request satisfies; available for Identity-Aware Proxy only, and only in bindings that grant a role containing solely the IAP accessViaIAP permissions
  • Forwarding rule attributes: compute.isForwardingRuleCreationOperation() and compute.matchLoadBalancingSchemes(); available for Cloud Load Balancing, Cloud VPN, Compute Engine protocol forwarding, and Cloud Service Mesh

Operators compare variable values against literal values: == and != for equality, <, >, <=, >= for numeric or timestamp comparison, and in for list membership. Note that resource.type and resource.service accept only == and !=; prefix or suffix comparisons against those two attributes give unexpected results. Avoid == and != on timestamps, which have millisecond precision.

Functions extend what operators can express, and are how you work with complex types like strings and timestamps:

  • String functions on resource.name: startsWith(prefix), endsWith(suffix), extract(extractionTemplate). There is no matches() function and no wildcard support in IAM Conditions; use extract() to pull a value such as a project ID out of a resource name
  • Timestamp functions: getHours(timezone), getDayOfWeek(timezone), getFullYear(timezone), getMonth(timezone), getDate(timezone), getDayOfMonth(timezone), getDayOfYear(timezone), getMinutes(timezone). The timezone argument is optional and defaults to UTC. Timestamps and durations are constructed with timestamp(), date(), and duration()
  • Tag matching: resource.matchTag(namespacedKeyName, valueShortName) or resource.matchTagId(tagKeyId, tagValueId), plus resource.hasTagKey() and resource.hasTagKeyId() to test for a key regardless of its value
  • API attribute access: api.getAttribute(attributeName, defaultValue), combined with hasOnly(list) to constrain which roles can be granted

Logical operators combine statements: && requires all to be true, || requires at least one to be true, and ! inverts a boolean expression.

CEL Examples by Use Case

The examples below are copy-pasteable. Code samples should be linted against current GCP APIs before publishing. (gcloud syntax verified against the current gcloud CLI reference, 12 August 2026.)

Time-based temporary access

Grant a role only until a specific expiry date:

Code
request.time < timestamp('EXPIRY_DATE')

Replace EXPIRY_DATE with an RFC 3339 timestamp (for example, 2027-03-01T00:00:00Z). Date strings alone are not valid as an argument to timestamp(); the time and timezone offset are required. If you want a bare date, use the date() function instead: date('2027-03-01') resolves to 2027-03-01 at 00:00:00 UTC. An invalid timestamp argument doesn't error at evaluation time, it simply never grants access.

gcloud equivalent:

bash
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="user:contractor@example.com" \
  --role="roles/bigquery.dataViewer" \
  --condition='expression=request.time < timestamp("EXPIRY_DATE"),title=Temp-access-expires-EXPIRY_DATE,description=Contractor read access'

Restrict a role to a specific resource type

Allow compute.admin on Compute instances only, not disks, networks, or other Compute resources:

Code
resource.type == 'compute.googleapis.com/Instance'

Resource type strings are case-sensitive. compute.googleapis.com/instance (lowercase i) evaluates to false against a Compute instance.

Scope access to objects within a specific Cloud Storage bucket

Code
resource.type == 'storage.googleapis.com/Object' &&
resource.name.startsWith('projects/_/buckets/prod-sensitive-data/')

Uniform bucket-level access must be enabled before you can add a condition to the bucket's own allow policy. If it isn't enabled, apply the conditional binding at the project level and let the bucket inherit it, but note that object ACLs can still grant access independently of the condition.

Business hours restriction

Grant access only on weekdays during business hours in the Europe/Berlin timezone:

Code
request.time.getHours('Europe/Berlin') >= 8 &&
request.time.getHours('Europe/Berlin') < 18 &&
request.time.getDayOfWeek('Europe/Berlin') >= 1 &&
request.time.getDayOfWeek('Europe/Berlin') <= 5

getDayOfWeek returns 0 for Sunday and 6 for Saturday, so 1 through 5 covers Monday through Friday. Timezone identifiers must match IANA Time Zone Database strings exactly. The timezone argument is optional; omit it and the function evaluates in UTC.

Tag-based access for environment scoping

Grant a role only for resources tagged as the staging environment:

Code
resource.matchTag('123456789012/env', 'staging')

The first argument to matchTag is the tag key's namespaced name: the organization's numeric ID, a forward slash, and the tag key short name. If your tag key "env" is defined at organization 123456789012, the namespaced name is 123456789012/env. Tag keys created at project scope are namespaced by the alphanumeric project ID, not the project number.

In a deny policy rule, to deny access when a resource is tagged as prod:

Code
resource.matchTag('123456789012/env', 'prod')

This is one of the few attribute forms that works in both allow and deny conditions.

Compound condition combining time, resource type, and name prefix

Scope a role to specific Compute instances in a specific zone, with a time boundary:

Code
request.time < timestamp('EXPIRY_DATE') &&
resource.type == 'compute.googleapis.com/Instance' &&
resource.name.startsWith('projects/my-project/zones/us-central1-a/instances/app-')

Constrain which roles a principal is allowed to grant

In a binding for roles/iam.projectIAMAdmin, restrict the principal to granting only viewer roles on storage:

Code
api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', [])
   .hasOnly(['roles/storage.objectViewer', 'roles/storage.legacyBucketReader'])

Limits, Quotas, and Known Constraints

ConstraintValueNotes
Max conditional role bindings per allow policy (best practice)100Exceeding this may push the policy past the overall size limit
Conditions on basic rolesNot supportedCannot condition Owner, Editor, or Viewer roles; grant a predefined or custom role instead. Google documents these as unusable with conditions and does not describe a silent-ignore behavior, so do not rely on the binding being accepted.
Conditions with allUsers or allAuthenticatedUsersNot supportedPublic principals cannot receive conditional bindings
Condition attributes in deny policiesResource tags onlyrequest.time, resource.type, and other attributes do not work in deny rule conditions. Deny rules fail closed: a condition that evaluates to true or cannot be evaluated causes the rule to apply.
Cloud Storage bucket-level conditionsRequire uniform bucket-level accessRequired to add a condition to the bucket's own allow policy. If it can't be enabled, put the conditional binding at the project level and let the bucket inherit it; object ACLs can still grant access independently.
getDayOfWeek return values0 (Sunday) through 6 (Saturday)Off-by-one is a common source of access window misconfiguration
getMonth return values0 (January) through 11 (December)Zero-indexed; getMonth() == 1 matches February, not January
Timestamp formatRFC 3339 required for timestamp()timestamp('2026-06-30') fails; timestamp('2026-06-30T00:00:00Z') is valid. date('2026-06-30') accepts a bare date and resolves to 00:00:00 UTC. An invalid argument never grants access.
getDate() vs. getDayOfMonth()One-based vs. zero-basedgetDate() returns 1 for the first day of the month; getDayOfMonth() returns 0 for the same day. The two are easy to swap.

Common Mistakes

Adding a conditional binding when an unconditional binding already exists. This is the most common and least visible error. A conditional binding for roles/storage.objectViewer does nothing if the same principal already holds roles/storage.objectViewer unconditionally. Conditions don't narrow unconditional grants; they're evaluated in addition to them, not instead of them. To scope access, remove the unconditional binding first.

Wrong resource type string. Resource type strings are case-sensitive and must match GCP's resource type identifiers exactly. "Compute.googleapis.com/Instance" (capital C) does not match "compute.googleapis.com/Instance". The condition evaluates to false silently, and the binding has no effect.

Using resource.name without a resource.type guard. Attributes are only populated for resource types that support them, and a part of a condition that references an unavailable attribute is never interpreted as granting access. If a role's permissions span resource types that don't expose resource.name, the resource.name clause silently withholds access for those types. Google recommends guarding the check with resource.type, not resource.service: (resource.type != 'storage.googleapis.com/Bucket' && resource.type != 'storage.googleapis.com/Object') || resource.name.startsWith('projects/_/buckets/example-bucket').

Miscounted day or month indices. getDayOfWeek and getMonth both use zero-based indexing. A condition intended to cover weekdays that uses getDayOfWeek >= 0 && getDayOfWeek <= 4 includes Sunday and excludes Friday. getMonth() == 1 is February, not January.

Using request.time or resource.type in a deny rule. Deny policy conditions only support resource tag functions. Other attributes cause the condition evaluation to fail. Test deny rule conditions in a non-production project before applying to production. Deny rules also fail closed: if the condition evaluates to true or cannot be evaluated, the deny rule applies and the permission is denied.

Not linting before deploying. The IAM API does accept syntactically valid CEL expressions that evaluate incorrectly at runtime. Lint expressions using the lintPolicy API method, or gcloud alpha iam policies lint-condition, before any conditional binding goes live. The lint command is still alpha as of August 2026, so pin the behavior you rely on.

Applying conditions to Cloud Storage without enabling uniform bucket-level access. Conditions can't be added to a bucket's own allow policy until uniform bucket-level access is on, so the real failure mode is subtler: the binding gets moved up to the project level, where it is accepted without complaint, and object ACLs on the non-uniform bucket keep granting the access the condition was written to scope out.

Best Practices

Use descriptive condition titles that include the scope and, for time-bounded access, the expiry. Titles appear in Cloud Audit Logs. A title like "Temp-access-expires-EXPIRY_DATE-analytics-read" tells you what the binding was for and when it should have ended. That context is what makes access reviews tractable at scale.

Prefer resource tags over resource.name for environment scoping. Name-prefix conditions work until your naming conventions change, someone creates a resource outside the expected prefix, or a project gets reorganized. Tag-based conditions (resource.matchTag()) are centrally managed and stay accurate as infrastructure evolves. Treat resource.name.startsWith() conditions as temporary scaffolding rather than a durable architecture choice.

Keep conditional bindings below 100 per policy, and treat the limit as a design signal. If you're approaching 100 conditional bindings on a single policy, the right response isn't to request a higher limit; it's to ask whether a broader condition expression at a higher level in the hierarchy could express the same intent more cleanly.

Include conditional bindings in your access review cycle as a distinct line item. Unconditional bindings are easy to audit. Conditional bindings require an extra check: has the condition expired, has the resource it references been renamed or removed, and does an unconditional binding for the same principal and role now exist alongside it? None of these states produce an error; they all produce silent access drift. A quarterly review that specifically asks these questions is more reliable than assuming conditions enforce themselves.

Store conditions in source control alongside your Terraform or other IaC definitions. Conditions that live only in the console are invisible to change management processes and prone to undocumented modification. Define the condition object in code, and treat any console change to a binding as a policy violation in the same way you would treat a manual change to a firewall rule.

Why Native

IAM Conditions give GCP security teams fine-grained access scoping at the binding level. The gap is keeping those conditions aligned with architectural intent as the environment evolves. Roles accumulate. Unconditional bindings persist alongside conditioned ones. Conditions expire or become stale without review. New resource types get added to a project without corresponding condition updates.

Native connects the conditions already present in your GCP environment to the architectural intent behind them, making it visible when conditions have drifted, when unconditional grants are bypassing intended scope, or when conditional bindings reference resource types that no longer exist in your environment. See how Native works across GCP, AWS, Azure, and OCI.

FAQ

Can IAM Conditions work with deny policies? Yes, but only with resource tag attributes. Deny rule conditions don't support request.time, resource.type, resource.name, or request attributes. If you need to scope a deny rule by time or resource type, the restriction needs to live in an allow policy binding instead.

What happens if a CEL expression has a syntax error? The IAM API will reject a syntactically invalid expression when you attempt to set the binding. A logically incorrect expression (valid syntax, wrong logic) will be accepted, evaluated at request time, and either grant or deny access based on the boolean result. This is why runtime testing matters beyond linting.

Can a binding have multiple separate conditions? No. Each role binding has one condition object. If you need different access windows or resource scopes for the same role, create separate bindings, each with its own condition.

Does a condition at the project level apply to all resources in the project? Yes. Conditional bindings at the project level apply to all resources in that project. The CEL expression is evaluated in the context of each individual access request, so resource.type and resource.name conditions still distinguish between resource types within that project.

Can conditions be used with service accounts as members? Yes. Service account principals (serviceAccount:name@project.iam.gserviceaccount.com) work the same as user principals in conditional bindings. This is how you scope CI/CD pipelines or agentic workloads to specific resource types or environments.

How do I condition on resource labels instead of tags? You can't, directly. Resource labels are not accessible as condition attributes. GCP's structured tags system (separate from key-value labels) is the supported mechanism for attribute-based conditions on resource metadata. Migrate to tags if label-based scoping is a requirement.

Ready to enforce secure-by-design?