Which Kyverno validate form do you standardise on when dozens of teams will patch the policy?
answer
- optimise for the reviewer, not the author
- who sends the patch
- one-word diffs that invert a rule
- CEL polarity runs the other way
- fixtures make subtle patches fail loudly
basics
~20 sChoose on reviewability, not expressiveness. Make the declarative pattern the default because governed teams can read it, allow deny conditions where operators or request fields are genuinely needed, and reserve the CEL form for teams already fluent in it.
solid answer
~60 sThe decision is about who reviews the patch, not which form is most powerful. A `validate.pattern` looks like the manifest the developer already writes, so a team sending a patch can see what changed; its ceiling is that it cannot express operators, set membership or anything outside the resource. A `validate.deny` block handles all of that, but it is verbose and its failure mode is silent — a patch that flips `any` to `all` or swaps `AnyIn` for `AnyNotIn` inverts the rule while looking like a one-word change. The CEL form is the most compact and gives each expression its own message, but it is a language your governed teams may not read, and its polarity is inverted relative to deny, so a rule ported between the forms is easy to get backwards. I would make pattern the default, require a written reason to move a rule to deny, keep CEL to the platform's own policies, and — whichever form — demand a fixture pair beside every policy so that a review of a subtle patch is backed by a test that actually fails.
go deeper
You are not expected to make this call, but know that Kyverno offers more than one way to write the same validate rule and that they differ in how easy they are to read.
Be able to say what each form can and cannot express — patterns have no operators, deny blocks reach request fields, CEL is compact with per-expression messages — and give one honest example of each.
Argue the choice for a specific rule and name the failure mode you are guarding against, particularly the silent inversion that a one-word patch to a deny block can cause.
Own the standard: which form is the default, what a rule must need before it may use a stronger one, what evidence accompanies every patch, and why a form teams cannot read produces exemptions rather than fixes.
## The question is really about patch review Once a policy governs dozens of teams, its author stops being the only person who edits it. Teams send patches to add an exemption, widen a set of approved values, or narrow a match. The form you standardise on decides whether those patches are reviewable by the people sending them and by whoever approves them at 5pm on a Friday. Expressiveness is a distant second consideration, because almost every rule anybody actually needs can be written in any of the three forms. ### The three forms and how a bad patch fails in each **`validate.pattern`** — a fragment of the desired resource that the object must satisfy. It reads like YAML the team already writes, so a patch to it is legible without knowing anything about policy authoring: someone adding a value or loosening a wildcard can see exactly what they changed. Its ceiling is real: no operators, no comparison between two fields, and no access to anything outside the object, such as the operation or the requesting user. When a patch to a pattern is wrong, it is usually wrong in a visible way — the shape no longer describes the resource anyone expected. **`validate.deny.conditions`** — an explicit list of `key`/`operator`/`value` conditions grouped under `any` or `all`, with the key normally a JMESPath expression over the admission request. This is the form that can express real logic: set membership over an arbitrary list of approved values, numeric comparison, a scope on the operation, an exemption for a named group. The cost is that its failure mode is silent and its diffs are deceptive. Changing `all` to `any` is a one-word patch that can turn a narrow rule into one that blocks half the estate; swapping `AnyNotIn` for `AnyIn` inverts a rule so that it blocks precisely the compliant resources; and because a deny rule produces no output unless it fires, an inverted rule and a compliant estate look identical in the reports. **The CEL form** — the validate rule can instead carry `cel.expressions`, each with an `expression` and its own `message`, following the upstream ValidatingAdmissionPolicy convention. It is the most compact of the three, and per-expression messages are a genuine usability win because a blocked developer gets told which specific check they failed rather than a single message covering a whole condition block. Two things make it a poor default for a widely-patched policy. It is a language: a team that does not write CEL cannot confidently patch it, and "I did not want to touch it" is how exemptions turn into copies of the policy. And its polarity is the opposite of a deny block — a CEL expression that is **true** means the resource is **allowed**, whereas deny conditions that are true **block**. A rule ported from one form to the other without negating it does exactly the wrong thing while reading plausibly. ### The call, and the conditions attached to it Make the pattern form the house default for policies that govern other teams. Allow `deny.conditions` where the rule genuinely needs an operator, a set, a cross-field comparison or a request field, and require the author to say which of those it is — that single sentence stops most of the drift towards the powerful form out of habit. Keep the CEL form for policies the platform team owns and patches itself, where the compactness and per-expression messages pay off and the readership is fluent. Then attach the things that make any of the three safe to patch: - **A fixture pair per policy**, checked in beside it: at least one resource that must be blocked and one that must pass, including one with the governed field absent entirely. This is what converts an inverted `any`/`all` from a silent regression into a failing check. - **A rollout order for rule changes**, so a widened rule is observed before it blocks. The mechanics of enforce-versus-audit belong to how the policy object is configured, but the norm — no rule change goes straight to blocking — is yours to set. - **A message that names the fix**, not the violation. A blocked team reads the message and nothing else; if it does not tell them the approved values, they will open a ticket instead of fixing the manifest, and the cost lands on your team. ### Where the argument usually goes The common objection is that standardising on the least powerful form means rewriting rules later. In practice the opposite bites harder: a policy nobody outside the security team can read accumulates exemptions rather than patches, because the cheapest safe change for a blocked team is always to get themselves excluded. The form that keeps teams patching the shared rule instead of escaping it is the one that pays, even when it occasionally forces a rule into a second rule rather than one clever condition block.
- What would make you accept the CEL form for a policy that many teams patch?That the teams already read CEL for other reasons, so it is not a new language just for policy. The payoff is per-expression messages — a blocked developer learns exactly which check failed instead of one message covering a whole condition block. I would still write the polarity in a comment above every expression, because true-allows is the reverse of the deny form and a ported rule is easy to get backwards.
- A team asks to change all to any in a shared deny rule. How do you review that?Not by reading the diff, which is one word. I would ask for the sentence the rule is meant to enforce and check the connective in it, then require the fixture pair to be updated in the same patch: the resource that must still be blocked, the one that must still pass, and one that is newly affected. If the change cannot be expressed as a fixture, it is not understood well enough to merge.
- How do you stop the more powerful form from spreading by habit?Require the reason in the patch: which capability the rule needs that a pattern cannot give it — an operator, a set of arbitrary values, a comparison across fields, or a request field such as the operation. It is a one-line justification and most drift does not survive being asked for it. It also gives future reviewers the rationale when they wonder whether the rule can be simplified back.
It is the same call as choosing the language a shared config is written in: the most expressive one wins on paper and loses the moment someone outside the owning team has to change it safely.
saying these in an interview costs you the question
- Picks the most expressive form on principle
- Ignores who will send patches to the rule
- Treats a one-word grouping change as a trivial diff
- Assumes CEL and deny share the same polarity
- Standardises a form with no fixtures behind it