Allow policies in Google Cloud only ever add access, so without IAM Conditions and deny policies the only way to limit a powerful role is to not grant it at all. IAM Conditions let you attach a Common Expression Language (CEL) expression to a role binding so it applies only to certain resources, times or tags, and deny policies let you block named permissions for named principals whatever roles they hold. This guide is the hands-on companion to the Google Cloud IAM and Service Accounts Guide: it covers writing conditions that do what you intend, the tag, time and API attributes worth knowing, how to roll out deny policies without breaking Google's own service agents, and where Principal Access Boundary policies fit. It is written for cloud platform, IAM and security engineers who already know basic Google Cloud IAM.
This is an independent guide from NHI Mgmt Group, based on Google Cloud's public IAM documentation as of November 2026. It is not sponsored or reviewed by Google. Product features change, so confirm settings against Google Cloud's current documentation.
Key takeaways
- Deny is evaluated first. IAM checks relevant deny policies before allow policies, and a deny set on an organization or folder cannot be undone by a more permissive policy lower down.
- Conditions narrow, they never override. A conditional binding does not limit an unconditional binding of the same role to the same principal, so remove the unconditional grant or the condition does nothing.
- Deny conditions understand tags only. Denial conditions use the resource tag functions; time, name and type attributes belong in allow bindings.
- Tag control is access control. When conditions depend on tags, whoever can bind tag values can change who has access, so govern tag permissions as tightly as IAM itself.
- Exempt service agents and break-glass accounts deliberately. Broad deny rules need exception principals for Google service agents and a tested emergency group, or they cause outages.
How conditions, deny and boundaries combine
Google Cloud now has three policy types that decide whether a request succeeds. Knowing which one to reach for is most of the design work.
| Policy type | Attached to | What it does | Condition attributes | Typical use |
|---|---|---|---|---|
| Allow policy with conditional bindings | Resource (one allow policy per resource), inherited down the hierarchy | Grants a role only when the CEL expression is true | Resource name, type, service and tags; request time; destination IP and port; request host and path; access levels; API attributes | Temporary access, per-bucket or per-prefix grants, tag-scoped grants, limiting which roles an IAM admin can grant |
| Deny policy | Organization, folder or project (up to 500 deny policies and 500 deny rules per resource), inherited | Blocks listed permissions for listed principals, with optional exceptions | Resource tag functions only | Guardrails such as "nobody outside the platform group can create custom roles" |
| Principal Access Boundary (PAB) policy | Principal sets through policy bindings, not inherited | Limits which projects, folders or organizations principals are eligible to access | principal.type and principal.subject on the binding | Keeping workforce or workload identities inside your own organization |
Two evaluation facts drive everything else. Deny rules are checked before allow policies, and each deny policy is evaluated independently. A deny rule with a condition applies when the condition is true or cannot be evaluated, so a deny condition fails closed. A PAB policy can only block: it makes resources eligible, and allow policies still have to grant the access. PAB policies also fail closed when evaluation errors occur.
Writing IAM Conditions that do what you intend
A condition has a required title, an optional description and a required expression written in a subset of CEL. Statements combine with &&, || and !, with a limit of 12 logical operators per expression in an allow binding or deny rule (10 in a PAB binding). Conditions cannot be used with the basic roles (Owner, Editor, Viewer) or with allUsers and allAuthenticatedUsers, and only some resource types accept conditional bindings.
Policy version 3 and the etag
Conditional bindings require allow policy version 3. If a client reads a policy without asking for version 3, IAM returns version 1, omits the condition and shows the role with a _withcond_ suffix and a hash. A version 1 write without the etag can overwrite the policy and delete all its conditions. The console and gcloud CLI set the version automatically; scripts and Terraform providers calling the REST API should send "requestedPolicyVersion": 3 and always round-trip the etag, retrying the whole read-modify-write on a 409 Conflict.
Resource attributes
- Always pair
resource.namewithresource.type:resource.nameis only populated for some resource types. A condition on an inherited binding that uses an attribute the target resource does not provide can silently fail to grant access. - No wildcards: use
startsWith(),endsWith(),extract(),==or!=.startsWith('projects/_/buckets/exampleco-site-assets/')matches objects in that bucket;==matches only an exact name. - Watch prefix over-match: Google's reference gives the example that
startsWith('security-admin')also matches[email protected]. End prefixes with a delimiter such as/. - Equality only on type and service:
resource.typeandresource.servicesupport only==and!=; preferresource.typewhen the condition also usesresource.name. - Cloud Storage: bucket-level conditions need uniform bucket-level access. The
storage.googleapis.com/objectListPrefixattribute works only in Credential Access Boundaries; putting it in a conditional role binding makes Cloud Storage methods fail. See the Google Cloud Storage Security Guide.
Time attributes
- Expiring grants:
request.time < timestamp('2026-12-31T23:59:59Z'). Use<,<=,>or>=; avoid==because timestamps carry millisecond precision. An invalid timestamp string means the condition never grants access. - Working hours: getters such as
getHours()andgetDayOfWeek()accept an IANA time zone, for examplerequest.time.getHours('Europe/London') >= 8. The default is UTC. Indexing differs:getDayOfWeek()starts at Sunday = 0 andgetHours()is zero-based, whilegetDate()is one-based. - Durations:
duration('3600s')takes seconds with anssuffix and can be added to or subtracted from a timestamp.
Expiring conditions are useful for one-off access, but for routine elevation the Google Cloud Privileged Access Manager Guide covers entitlements with approval and audit, which can themselves carry conditions.
Tag attributes
resource.matchTag('123456789012/env', 'prod') and resource.hasTagKey('123456789012/env') use namespaced names; resource.matchTagId() and resource.hasTagKeyId() use permanent IDs. Google's guidance on the trade-off is direct: names are easier to read and manage declaratively, but a deleted and recreated tag with the same name inherits the old conditions, which can grant unintended access. Permanent IDs avoid that, at the cost of updating policies when tags are recreated. For production guardrails, prefer IDs.
API attributes: limiting what an IAM admin can grant
The iam.googleapis.com/modifiedGrantsByRole attribute lists the roles a set-allow-policy request changes. A binding for a project IAM admin with the condition api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/pubsub.editor', 'roles/pubsub.publisher']) lets that admin grant only those two roles. The attribute is accepted on projects, folders and organizations and recognised by services including Compute Engine, Cloud Run, Cloud Storage, Secret Manager and Resource Manager. Where a service does not recognise it, the limited admin's grant on that resource fails; BigQuery datasets are a documented exception, where dataset role grants are not limited. This is the cleanest way to delegate IAM administration to application teams without handing them T1098.003 Additional Cloud Roles as a privilege escalation path.
Condition patterns reference
| Goal | Expression pattern | Where it works | Watch out for |
|---|---|---|---|
| Grant expires at a fixed time | request.time < timestamp('2026-12-31T23:59:59Z') | Allow bindings | Expired bindings stay in the policy; clean them up |
| Office hours only | request.time.getHours('Europe/London') >= 8 && request.time.getHours('Europe/London') < 18 | Allow bindings | Zero-based hours; time zone defaults to UTC |
| Only Compute Engine instances | resource.type == 'compute.googleapis.com/Instance' | Allow bindings | Other resource types in the project get nothing from this binding |
| Objects under one bucket | resource.type == 'storage.googleapis.com/Object' && resource.name.startsWith('projects/_/buckets/BUCKET/') | Allow bindings | Trailing slash; uniform bucket-level access |
| Only resources tagged non-production | resource.matchTagId('tagKeys/KEY_ID', 'tagValues/VALUE_ID') | Allow bindings and deny rules | Tag binding permissions decide access |
| Deny everywhere except test | !resource.matchTag('ORG_ID/env', 'test') | Deny rules | Untagged resources are denied too; that is usually the intent |
| Limit which roles an admin can grant | api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([...]) | Allow bindings for IAM admin roles | Services that do not recognise the attribute block the grant |
| IAP access from managed devices | 'accessPolicies/NUM/accessLevels/LEVEL' in request.auth.access_levels | Bindings of roles/iap.tunnelResourceAccessor or roles/iap.httpsResourceAccessor | Access level name is case sensitive; role must contain only the IAP access permissions |
| Apply a PAB policy only to service accounts | principal.type == 'iam.googleapis.com/ServiceAccount' | PAB policy bindings | Only principal.type and principal.subject are available |
Keep conditional bindings under about 100 per allow policy, as Google recommends, and remember the limit of 20 bindings for the same role and principal with different conditions.
Authoring deny policies
A deny policy contains rules, and each rule has deniedPrincipals, optional exceptionPrincipals, deniedPermissions, optional exceptionPermissions and an optional denialCondition. Deny basics, such as why deny exists at all, are covered in the Google Cloud IAM and Service Accounts Guide; the details below are the ones that trip up authors.
- Permission format: deny rules use the v2 format
SERVICE_FQDN/RESOURCE.ACTION, so the familiarserviceusage.apiKeys.regeneratebecomesapikeys.googleapis.com/apiKeys.regenerate, and creating custom roles isiam.googleapis.com/roles.create. Only some permissions can be denied: check each one in Google's supported permissions table. - Permission groups:
SERVICE_FQDN/RESOURCE.*,SERVICE_FQDN/*.*andSERVICE_FQDN/*.VERBare accepted for certain services and include permissions added later. That future coverage is useful for guardrails but means a rule can start blocking a new feature without anyone changing the policy. - Principal identifiers: deny rules use IAM v2 identifiers such as
principal://goog/subject/[email protected],principalSet://goog/group/[email protected]andprincipalSet://goog/public:all(everyone). Groups keep rules short and stay inside the limit of 500 groups and domains and 2,500 principals across a resource's deny policies. - Exceptions, not carve-outs in allow: because deny wins, the only way to let the platform team through is
exceptionPrincipals. Use one well-governed group per rule. - Service agents: when the denied set includes service agents, for example
principalSet://goog/public:all, Google advises adding your service agents as exception principals so Google-managed services keep working. - Who manages deny: Deny Admin (
roles/iam.denyAdmin) can create, update and delete deny policies and Deny Reviewer (roles/iam.denyReviewer) can view them; both are granted on the organization. Keep Deny Admin to a very small group outside day-to-day project administration.
Policies are created with gcloud iam policies create POLICY_ID --attachment-point=cloudresourcemanager.googleapis.com/projects/PROJECT_ID --kind=denypolicies --policy-file=policy.json. Changes generally take effect within two minutes but can take seven minutes or more, so do not test immediately after a change and assume the result is final.
| Guardrail | Denied permissions (v2 format) | Denied principals | Exceptions |
|---|---|---|---|
| Only the IAM platform team manages custom roles | iam.googleapis.com/roles.create, iam.googleapis.com/roles.delete and related role permissions | principalSet://goog/public:all | IAM platform group, break-glass group, service agents |
| No API key regeneration outside the owning team | apikeys.googleapis.com/apiKeys.regenerate | All principals | Owning team group |
| Protect production from deletion | Project and resource deletion permissions confirmed in the supported permissions table | All principals | Change automation service account, break-glass group; condition on the production tag |
Rolling out deny policies safely
Because a deny rule takes effect for every matching request as soon as it propagates, do the testing before you widen the attachment point.
- Design from evidence: list the permissions you want to block and confirm each is deniable. Use Cloud Audit Logs to see who actually uses them today; see the Google Cloud Audit Logs and Log Sinks Guide.
- Create the exception groups first: a platform group, a break-glass group whose use pages the security team, and the service agent exceptions. See the Break-Glass Account Guide.
- Pilot on a sandbox project: attach the policy at a test project, wait for propagation and test with a denied and an excepted identity.
- Check with Policy Troubleshooter:
gcloud policy-intelligence troubleshoot-policy iam RESOURCE --principal-email=EMAIL --permission=PERMISSIONevaluates allow and deny policies on the resource and its ancestors (the beta version adds PAB, in Preview). Tag conditions are evaluated automatically; time and name conditions need context you supply. Running it needs Security Reviewer and Deny Reviewer. - Widen in rings: move the attachment point from project to a non-production folder, then to production folders, then to the organization, leaving time between steps for teams to report breakage.
- Keep it as code: store policy files in version control with review, and include the etag in updates. See the Policy-as-Code for Authorisation Guide.
- Rollback: deleting or editing the deny policy restores access once the change propagates. Make sure Deny Admin holders can act out of hours, because they are the only people who can undo a bad rule.
Tags as an access control dependency
Tags are inherited from organization, folder and project, a child can override an inherited value with a different value for the same key, and tag changes take effect in about two to seven minutes. That makes them powerful for conditions and also means binding or unbinding a tag, or moving a project, can change access. Google recommends binding a safe default value for each key used in conditions at the organization (for example enforcement: off) and overriding it only on specific resources. Treat the permission to bind tag values (granted through the Resource Manager tag user role) as a privileged permission, scope it to the specific tag values a team needs, and review tags and conditional policies before moving resources between folders. Keep sensitive data out of tag names and values, as Google advises. The Google Cloud Organization Policy Guide covers organization policy constraints that can also reference tags, including mandatory tags (in Preview).
Principal Access Boundary in practice
PAB policies answer a different question: not "may this principal do this?" but "which resources is this principal eligible to touch at all?". Each rule lists projects, folders or organizations with the only valid effect, ALLOW; bindings attach policies to principal sets such as workforce or workload identity pools, Google Workspace domains, or project, folder and organization principal sets (and agent identities). A principal set can have up to 10 PAB policies, and eligibility is the union of them.
- Pin the enforcement version:
details.enforcementVersioncontrols which permissions the policy can block. Usinglatestcan remove access unexpectedly as new permissions are added. - Do not leave principals unbound by accident: a principal subject to no PAB policy is eligible for everything, so when narrowing, keep each principal under at least one policy.
- Google-owned resources: principals under PAB lose access to Google-owned public resources (organization
433637338589, which holds resources such as BigQuery public datasets) unless a policy makes that organization eligible. - Agents and workloads: PAB is a strong fit for workload identity federation pools and AI agent identities that should never act outside your organization. See the Google Cloud Agent Identity guide.
Where identity fits
The principals most in need of conditions and deny rules are usually non-human: CI/CD service accounts that need a deployer role only on tagged environments, automation that should never create service account keys or custom roles, and federated workloads that should stay inside one organization. Write deny rules against principal sets that include service accounts, use PAB conditions on principal.type to target service accounts, and keep exceptions to named automation identities with owners. The Top 10 GCP IAM Mistakes page lists the over-privilege patterns these controls are meant to stop.
Detection and measurement
- Policy changes: alert on any create, update or delete of deny policies and PAB policies and bindings, and on SetIamPolicy calls that remove conditions or add unconditional bindings for roles you normally grant conditionally (T1098 Account Manipulation).
- Tag changes: alert on tag binding changes for keys used in conditions, and on project moves between folders.
- Denied requests: watch PERMISSION_DENIED errors after each rollout ring; the Google Cloud PERMISSION_DENIED Troubleshooting Guide covers telling deny, PAB and missing-allow failures apart.
- Break-glass use: every action by an exception principal on a guarded permission should open a ticket.
| Metric | Target |
|---|---|
| Guardrail permissions (custom roles, key creation, deletion of production) covered by an organization or folder deny rule | 100 percent of the agreed list |
| Unconditional bindings of roles your standard says must be conditional | Zero |
| Expired time-bound bindings still present in allow policies | Zero after the monthly clean-up |
| Holders of Deny Admin and tag administration roles | Named, reviewed quarterly, kept to a handful |
| Workload identity pools and agent identities bound to a PAB policy | 100 percent |
Common failure patterns
- Condition added, unconditional grant left in place. The conditional binding looks restrictive in review but the older unconditional binding of the same role still applies.
- Version 1 tooling stripping conditions. A script that reads without
requestedPolicyVersion: 3and writes without the etag silently deletes every condition on the policy. - Name-based tags recreated. A tag deleted and recreated with the same namespaced name inherits conditions written for the old one.
- Deny on everyone without service agent exceptions. Managed services that act through service agents start failing hours after rollout, when a scheduled job first needs the permission.
- Prefix conditions without a delimiter.
startsWith('projects/_/buckets/prod')also matchesprod-archiveand any other bucket sharing the prefix. - Testing too soon. Engineers test a deny policy seconds after creating it, see access still working, and widen the rule before propagation completes.
Framework mapping
| Control | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Conditional and time-bound grants | AC-2, AC-6 | PR.AA-05 | 6 | 5.15, 5.18 |
| Deny guardrails and PAB boundaries | AC-3 | PR.AA-05 | 6 | 8.2, 8.3 |
| Limiting delegated IAM administration | AC-6 | PR.AA-05 | 5 | 8.2 |
| Policy as code with review and rollback | CM-3 | GV.RR | 4 | 8.32 |
Practitioner checklist
- Make every API client request policy version 3 and send the etag on writes.
- Pair every
resource.namecondition with aresource.typecheck and end prefixes with a delimiter. - Remove unconditional bindings that shadow conditional ones.
- Use permanent tag IDs in production conditions and bind a safe default tag value at the organization.
- Restrict tag binding permissions to the values each team needs.
- Limit delegated IAM admins with
modifiedGrantsByRoleandhasOnly(). - Write deny rules in v2 permission format, confirm each permission is deniable, and add service agent and break-glass exceptions.
- Pilot deny policies on a sandbox project, verify with Policy Troubleshooter, then widen folder by folder.
- Bind PAB policies to workload identity pools and agent identities with a pinned enforcement version.
- Alert on changes to deny policies, PAB bindings, tags used in conditions and Deny Admin membership.
Standards and references
- Google Cloud: Deny policies overview
- Google Cloud: Deny access to resources
- Google Cloud: Overview of IAM Conditions
- Google Cloud: Attribute reference for IAM Conditions
- Google Cloud: Principal Access Boundary policies
- Google Cloud: Understanding allow policies (policy versions)
- Google Cloud: Tags and access control
- Google Cloud: IAM quotas and limits
Related NHI Mgmt Group resources: Google Cloud IAM and Service Accounts Guide · Google Cloud Organization Policy Guide · Top 10 GCP IAM Mistakes · Google Cloud Privileged Access Manager Guide