IAM Conditions
An IAM condition attaches a logical test to a role binding so the grant only applies when the test is true — a way to add a constraint to who-gets-what without inventing a narrower role. You write the test as a CEL (Common Expression Language) expression over attributes like the time of day, the resource's name, or its type, drop it in a condition {} block on the binding, and the role applies only on requests where CEL evaluates true.
The catch is the part to internalize before you rely on one: conditions are evaluated per-request, and not every service or permission supports them. A condition on an unsupported permission silently does nothing — the role applies unconditionally, the opposite of the tightening you intended. Conditions fail open, so you verify support before trusting one.
Conditional Bindings
A condition {} block on a google_*_iam_member or google_*_iam_binding carries a title, an optional description, and a CEL expression. The binding grants the role only on requests where the expression evaluates true, turning one role into a context-scoped grant. The same predefined role you already understand becomes "this role, but only here" or "only until then" — no new role, just a test on the existing binding.
One subtlety the model demands: the conditional binding is a different binding from the unconditional one. If you grant the same role to the same member once with a condition and once without, the unconditional grant already applies everywhere, so the condition buys nothing. A condition narrows only the binding it sits on.
CEL on Resource Attributes
The attributes resource.name, resource.type, and resource.service let you scope a grant to a name prefix or a single resource. Granting roles/storage.objectViewer at the project level with a condition on resource.name.startsWith('projects/_/buckets/hatch-events-raw') makes a broad project grant behave like a per-bucket one — the role only applies when the request targets that bucket. The configuration below does exactly that.
resource "google_project_iam_member" "raw_viewer" { project = "hatch-data-prod" role = "roles/storage.objectViewer" member = "group:gcp-developers@hatch.io" condition { title = "raw-bucket-only" expression = "resource.name.startsWith('projects/_/buckets/hatch-events-raw')" } }
Get the fully-qualified format right or the CEL never matches. projects/_/buckets/hatch-events-raw is the form Cloud Storage requests carry; a bare bucket name will not match, and the grant then effectively never applies — a quiet failure in the safe direction, but still a bug.
CEL on Request Attributes
request.time enables time-bounded access: a grant that expires after a date, or one that only applies during business hours. This is the standard pattern for temporary elevated access — grant an on-call group a stronger role with a condition that request.time < timestamp('2026-07-01T00:00:00Z'), and the elevation stops granting on its own when the window closes, without a person remembering to revoke it.
The condition stopping is not the same as the binding being gone. Once the window passes, the binding still exists in your config and in the policy — it just grants nothing. That dead binding is audit noise, so the practice is to remove it when the need ends, not to leave expired conditions accumulating.
Limitations and Silent No-Ops
Conditions are supported on a defined subset of services and permissions, not universally. Attach one to a permission that does not support conditions and the condition is ignored — the role applies unconditionally, granting more than you wrote, not less. This is the failure mode that makes conditions dangerous to assume: a tightening you believed you applied silently is not there. Verify in the docs that the target permission supports conditions before relying on one, rather than assuming it does.
Conditions vs Deny Policies
A condition narrows a positive grant — it allows only when the test is true. It cannot create an exception to an inherited allow, because it only shapes the binding it sits on. When you need an actual carve-out — deny this even though an ancestor allows it — the mechanism is an IAM deny policy, a separate construct that denies regardless of allows. Conditions and deny policies solve the two different halves of "this person, but not here": conditions scope an allow down, deny policies subtract from one. Reach for a deny policy when you mean to take something away.
IAM condition — narrows a positive grant: the role applies only when the CEL test is true. Use it to scope an allow down to a resource, a name prefix, or a time window. It cannot subtract from an inherited allow.
IAM deny policy — a separate construct that denies a permission regardless of what allows exist, including inherited ones. Use it for an actual carve-out — "this group, but never on these resources" — which a condition cannot express.
- Attaching a condition to a permission that does not support conditions and assuming the grant is now scoped — it is silently ignored and the role applies unconditionally, the opposite of the intended tightening.
- Treating two bindings of the same role to the same member — one conditional, one not — as additive narrowing; the unconditional one already grants everywhere, so the condition buys nothing.
- Writing a
resource.namecondition with the wrong fully-qualified format (projects/_/buckets/...versus a bare name) so the CEL never matches and the grant effectively never applies. - Using
request.timefor "temporary" access but never removing the binding — the expired condition stops granting, yet the dead binding lingers as audit noise and confusion. - Reaching for a condition to create an exception to an inherited allow — a condition only shapes its own binding; an actual carve-out needs an IAM deny policy.
- Verify the target service and permission support IAM conditions before relying on one — confirm in the docs, because an unsupported condition fails open.
- Use
request.timeconditions for time-bounded elevated access instead of permanent broad grants, and still remove the binding when the need ends. - Scope project- or folder-level grants down to specific resources with
resource.nameorresource.typeCEL when a predefined role is broader than the job. - Reach for IAM deny policies, not conditions, when you need an actual exception to an inherited allow.
- Give every condition a clear
titleso an audit can read the intent of the binding without parsing the CEL.
Knowledge Check
You attach a condition to a permission that does not support IAM conditions. What happens?
- The condition is silently ignored and the role applies unconditionally — it fails open, granting more than intended
- The apply errors out at plan time, refusing to create a conditional binding on a permission that does not support conditions
- The grant is denied entirely until the condition is removed
- The condition is converted into a deny policy automatically
Which CEL attribute would you use to scope a grant to a time window for temporary elevated access?
request.time, comparing against a timestamp so the grant stops applying after the windowresource.name, matching a bucket or object name prefix so the grant follows the resourceresource.type, matching the resource's kind such as a Compute instance or bucketrequest.region, matching the caller's geographic location at request time
You grant a group roles/storage.objectViewer once unconditionally and once with a resource.name condition. What is the effect?
- The unconditional grant already applies everywhere, so the conditional one narrows nothing — the condition buys nothing
- The conditional binding takes precedence over the unconditional one and scopes the group's access down to the named resource
- The two bindings cancel each other out and the group loses the objectViewer role on every bucket entirely
- Access is granted only where both bindings agree, intersecting the two
You need to deny a group access to specific resources even though an ancestor grants it. Condition or deny policy?
- A deny policy — a condition can only narrow its own positive binding and cannot subtract from an inherited allow
- A condition with a negated CEL expression on the child binding, which reaches up and overrides the ancestor grant
- Either works identically; conditions and deny policies are interchangeable
- Neither — inherited allows cannot be carved out by any mechanism
You got correct