Home Blog GCP Architecture Series #36 — Deny Policies: Syntax, Evaluation Order and Denial Conditions…
GCP Architecture GCP Architecture Series

GCP Architecture Series #36 — Deny Policies: Syntax, Evaluation Order and Denial Conditions

#33 established that an allow policy can only add, which is why a grant at the organization cannot be narrowed lower down. Deny policies are the subtraction — but they are a separate machine with their own permission format, their own wildcard rules, their own condition language and their own failure direction, and treating them as allow-with-a-minus-sign is how they get misconfigured.

Verified against current vendor documentation on 18 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

#35 ended on the observation that the deny policy permissions are among the handful you cannot put in a custom role. This post is about what that restriction protects.

1
"We wrote the permission the way we always do"

Then the rule does not do what you think. Deny permissions use the IAM v2 permission format, which uses fully qualified domain names — iam.googleapis.com/roles.delete, not iam.roles.delete. One product, two permission syntaxes, depending on which policy you are writing.

Correct approach

Translate every permission into SERVICE_FQDN/RESOURCE.ACTION when moving from an allow policy to a deny rule, and never copy a permission name between the two unchanged.

2
"The condition will just skip the rule if the tag is missing"

The opposite. If the condition evaluates to true or cannot be evaluated, the deny rule applies. An unevaluable condition is not an inert rule — it is an active denial.

Correct approach

Design conditions so the unevaluable case is the one you want. Deny fails closed, which is correct for a guardrail and dangerous if you assumed otherwise.

3
"We will condition the deny on the caller's IP and the time of day"

You cannot. Denial conditions have the same structure as IAM Conditions, but denial conditions only recognize resource tag functions. The rich attribute set from #37 is not available here.

Correct approach

Express the carve-out as a tag on the resources, and condition on that. If it cannot be described by a tag, it cannot be a denial condition.

4
"We denied it on the specific bucket"

There is no such attachment. Each deny policy is attached to an organization, folder, or project — the resource-level attachment that allow policies support does not exist for deny.

Correct approach

Attach at the lowest of the three that contains what you mean, and use a tag-based denial condition to narrow further within it.

Architecture

A deny policy is metadata plus deny rules, and a deny rule is five fields. The fields are simple; the asymmetries with allow policies are the substance.

Diagram: the five fields of a Google Cloud IAM deny rule, the evaluation order placing deny before allow, the four ways deny policies are deliberately asymmetrical with allow policies, and the fail-closed behaviour of a denial condition that cannot be evaluated
Five fields, one evaluation order, and four asymmetries that make deny a different machine rather than an inverted one.

The five fields of a deny rule

FieldWhat it does
deniedPrincipals Who loses the permission. Individuals or sets, up to and including all users on the internet.
exceptionPrincipals Who is exempt. These principals are not denied the specified permissions even if they are listed in deniedPrincipals — or are in a group that is.
deniedPermissions What is denied, in v2 FQDN format. Only some permissions can be denied.
exceptionPermissions Permissions carved back out, typically to punch a hole in a permission group.
denialCondition When the rule applies. Tag functions only, and it fails closed.

The two exception* fields are what make deny usable in practice. The idiomatic guardrail is not a narrow denial of a few people — it is a total denial with a named exemption: deny principalSet://goog/public:all, then list the one group that should still be able to act. That reads as "only this team may do this", expressed as a deny.

Evaluation order

One sentence governs everything: IAM always checks relevant deny policies before checking relevant allow policies. There is no combination of roles that overcomes a matching deny rule, which is precisely what #33 could not offer. It also means a deny policy cannot be debugged by reading allow policies, because the allow side is never consulted once a deny matches.

Deny policies inherit like allow policies do — attach one and the policy is also effective for all resources inside that project, folder, or organization. And each deny policy is evaluated independently of all other deny policies, so a resource may carry up to 500 deny policies, and together these deny policies can contain a total of 500 deny rules.

The field that fails closed

Every other optional field in IAM narrows a rule when it is absent or unsatisfied. The denial condition does the reverse: if it evaluates to true or cannot be evaluated, the rule applies, and only an explicit false spares the principal. That is the right default for a guardrail — a control that switches itself off when confused is not a control. But combine an unevaluable condition with public:all in deniedPrincipals and the result is everyone denied that permission across the attachment point and everything under it, from a rule that was supposed to apply narrowly. Test denial conditions against resources that lack the tag, not only against ones that carry it.

Why This Architecture Holds Up

It would be reasonable to expect deny to be allow with the sign flipped. It is not, in four specific ways, and each one is a decision rather than an oversight.

Allow policyDeny policy
Permission format iam.roles.delete iam.googleapis.com/roles.delete — v2, FQDN.
Wildcards Never, in any role. Yes, in supported permission groups.
Attachment Organization, folder, project, or resource. Organization, folder, project only.
Conditions The full IAM Conditions attribute set. Resource tag functions only, failing closed.

The wildcard asymmetry is the interesting one

#35 established that a custom role cannot use wildcards, so every permission is listed explicitly and the list ages: when a service gains a permission, the custom role does not. Deny policies invert this exactly. Permission groups include all current and future permissions that match the specified pattern — deny example.googleapis.com/exampleResource.* today and tomorrow's new permission on that resource type is denied automatically, with no edit.

Put those two facts together and the shape of the design appears. Grants do not grow on their own; prohibitions do. A permission added to a service next quarter arrives ungranted by your custom roles and already covered by your deny rules. Whether that was the intent or a happy consequence of two teams making locally sensible choices, it is the right asymmetry, and it is the strongest argument for expressing a guardrail as a deny policy rather than as the absence of a grant.

The restriction on that power is narrow and explicit: you can only use wildcards in supported permission groups, and using wildcards in other permission names is not supported. So the future-proofing applies where Google has defined a group, not wherever you would like it.

What to do with this

  1. Write guardrails as deny-all-with-exceptions. public:all in deniedPrincipals, the responsible group in exceptionPrincipals. It survives new people, new roles and new projects.
  2. Prefer permission groups to permission lists where a group exists, because the group covers permissions that do not exist yet.
  3. Test every denial condition against an untagged resource. That is the case that fails closed, and it is the one nobody tries.
  4. Do not reach for deny to fix a bad grant. A deny rule masking an over-broad role leaves the role in place for #34 to trip over later; fix the grant and use deny for policy you want to hold regardless.
  5. Budget the 500. Rules are the scarcer resource — 500 across all policies on a resource, however you distribute them.
  6. Remember it is eventually consistent, exactly as in #33. A new deny is not instantaneous protection.

Key Architecture Decisions

DecisionChoose thisBecause
Making a restriction unconditional A deny policy IAM always checks deny policies before allow policies.
Writing the permission SERVICE_FQDN/RESOURCE.ACTION Deny uses the IAM v2 permission format with fully qualified domain names.
Covering future permissions A permission group Groups include all current and future permissions matching the pattern.
Wildcards outside a group Not available You can only use wildcards in supported permission groups.
Restricting an admin action to one team Deny all, except that group Exception principals are not denied even when listed in deniedPrincipals.
Narrowing below project level A tag-based denial condition Deny policies attach only to an organization, folder, or project.
Conditioning on request attributes Not possible in deny Denial conditions only recognize resource tag functions.
Handling an unevaluable condition Assume it denies If it cannot be evaluated, the deny rule applies.
Organising many rules Several policies, one concern each Each deny policy is evaluated independently of all others.
Assuming a permission is deniable Check first Only some permissions can be denied.

Closing Thought

Three posts ago the union rule looked like a limitation: access accumulates, nothing subtracts, and a grant made at the organization cannot be taken back lower down. Deny policies resolve that, and the way they resolve it is more interesting than the fact that they do. They are not a negative binding inside the allow policy, which would have been the obvious design. They are a separate policy type, evaluated first, with a different permission syntax, a different attachment model and a deliberately impoverished condition language.

That separation is the feature. An allow policy is a statement about what someone may do, and it is edited constantly by many people across the hierarchy. A deny policy is a statement about what nobody may do, it lives at three levels only, it fails closed, and — per #35 — it cannot be administered through a custom role you built yourself. Every one of those choices makes it harder to change casually. For a mechanism whose whole purpose is to hold when everything else has been granted away, harder to change is the point.

Next in this series

#37 opens up the condition language that deny policies deliberately do without: IAM Conditions — attribute-based access control, what you can actually test on a request, and why a conditional binding is still only ever an addition.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent