Module 6: Runtime Security Admission Control And Image Scanning
5. Kyverno: the YAML-native alternative
Description
Gatekeeper solves this guide's problem — rejecting a Pod with no resource limits — with two new objects and a new language (Rego). Kyverno solves exactly the same problem with a single object (ClusterPolicy) written entirely in YAML — the same format you've already written every Deployment, Service, and NetworkPolicy in this guide in, with no additional syntax to learn. This lesson explains why two complete engines exist for the same problem, and why this guide declares no winner between them.
Connection to the module
Lesson 6 is going to install Kyverno v1.18.2 and write the policy equivalent to lesson 4's, tested with the same two Pods. This lesson, first, makes clear what changes and what doesn't change between the two engines — because what does not change matters more than it seems: both are preventive admission controllers, both connect to the same ValidatingWebhookConfiguration lesson 2 described, both evaluate the same AdmissionReview at the same instant in a request's lifecycle. The only thing that changes is the language you tell the engine what to check in.
Analogy: two languages for writing the same gatekeeper rule
Lesson 1's gatekeeper now has two equally valid ways of writing their rulebook. They could write it in a formal logic language — "for every element X that enters, if X lacks attribute Y, then reject X" — which requires learning its specific grammar, but lets them express arbitrarily complex rules, even combining information from sources outside the list itself. Or they could write it as a direct checklist — "check that the suitcase has: (1) a tag with the destination, (2) a locked latch" — which anyone who already knows how to read a shopping list understands with no extra training, but starts getting hard to read the moment the rule needs logic more elaborate than a simple "this must be present" list. Neither form is objectively superior: the first (Rego, Gatekeeper) gives the gatekeeper a general-purpose language, reusable for checking suitcases, documents, or anything else they might ever need to evaluate; the second (YAML, Kyverno) gives them a list any other gatekeeper — with no prior training in formal logic — can read, understand, and maintain the same day they start working.
The same idea, two complete syntaxes side by side
Before installing anything (that's lesson 6), it's worth seeing, in parallel, how each engine would express "every container must declare resources.limits and resources.requests for cpu and memory":
GATEKEEPER (Rego inside a CRD) KYVERNO (pure YAML)
ConstraintTemplate ClusterPolicy
├─ defines a new CRD └─ does NOT define any new CRD —
│ (K8sRequiredResources) uses its own type, already installed
├─ contains Rego logic └─ contains a YAML pattern
│ (violation[...] { ... }) (spec.validate.pattern)
│ structurally compared
Constraint against the incoming object
└─ instance of the CRD (a single piece, not two)
with match + parameters
(two separate pieces)
And, in real code, side by side:
# Gatekeeper — the logic lives in the ConstraintTemplate (lesson 3/4)
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
required := input.parameters.limits
provided := {key | container.resources.limits[key]}
missing := {key | key := required[_]; not provided[key]}
count(missing) > 0
msg := sprintf("container <%v> is missing required resource limits: %v", [container.name, missing])
}
# Kyverno — the complete rule lives in a single object (lesson 6 installs it)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: andes-cargo-require-resource-limits
spec:
validationFailureAction: Enforce
rules:
- name: require-resource-requests-and-limits
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["andes-cargo"]
validate:
message: "every container must set resources.requests and resources.limits for cpu and memory"
pattern:
spec:
containers:
- resources:
requests: { cpu: "?*", memory: "?*" }
limits: { cpu: "?*", memory: "?*" }
Three structural differences, all visible in that code:
- Gatekeeper needs two objects (
ConstraintTemplate+Constraint); Kyverno needs just one (ClusterPolicy). There's no step equivalent to "generate a new CRD" in Kyverno —ClusterPolicyis already a CRD Kyverno installs once, for all future policies, and every newClusterPolicyis simply one more instance of that same type, with its own logic inside. - Rego describes a condition that makes a violation true; Kyverno's
patterndescribes the shape the object must have. The"?*"operator in Kyverno's YAML literally means "any non-empty value" — ifresources.limits.cpudoesn't exist, or exists but is empty, the pattern doesn't match and the rule fails. It's structural-matching logic (does this object have this shape?), not declarative logic over arbitrary conditions (under what combination of facts is this true?). validationFailureAction: Enforcein Kyverno is, in spirit, the same switch asenforcementAction: denyin a GatekeeperConstraint— both engines offer an "audit only, don't block" mode (Auditin Kyverno,dryrunin Gatekeeper) to test a new policy with no risk of breaking anything in production before trusting it.
What Rego can do that Kyverno's pattern, by design, doesn't do as directly
Kyverno's pattern is extraordinarily readable for this guide's case — "does this field exist, yes or no?" — but its strength (simple structural comparison) is also its limit: expressing a condition that depends on combining several fields with arbitrary logic (for example, "the memory limit must be at least double the memory request," a comparison between two values, not just each one's presence) starts needing Kyverno's more advanced deny/validate expressions — JMESPath, or directly CEL (Common Expression Language) in the project's more recent policies — which, in practice, get closer and closer in expressiveness to what Rego already did out of the box from the start. Rego, by design, doesn't have that limit: it was built, from the start, as a general-purpose language for arbitrarily complex policy logic, at the cost of a steeper learning curve.
When a team picks each one — with no declared winner
Neither of the following two lists is a recommendation from this guide. They're the real criteria, documented by both projects and by industry practice, that tilt the decision one way or the other:
A team tends toward Gatekeeper when:
- It already uses OPA/Rego elsewhere in the stack (for example,
conftestoverterraform plan, likecloud-security-and-guardrails-guide) and wants to reuse the same language knowledge in both contexts. - It needs policies with complex combinatorial logic — crossing several fields, iterating over nested structures with conditions that depend on each other — where a general-purpose language's full expressiveness is worth the cost of learning it.
- It wants a policy engine potentially reusable beyond Kubernetes — the same OPA that evaluates a
Podcan, in other contexts outside this guide, evaluate an API response or a configuration file from a different system, with no engine change.
A team tends toward Kyverno when:
- Most of the team already reads and writes Kubernetes YAML fluently, but no one on the team knows Rego — the training cost of a new policy is, literally, zero for someone who already writes Kubernetes manifests every day.
- The needed policies are mostly of the "does this field exist?", "does this value match this pattern?" shape — the most common case in practice (requiring labels, requiring resource limits, forbidding the
latesttag, requiringreadOnlyRootFilesystem: true), all expressible with no complex combinatorial logic. - It needs, in addition to validating, to mutate incoming objects (injecting a missing label, adding a default value) or generate derived resources automatically (for example, a default
NetworkPolicyfor every new namespace) — Kyverno built these three capabilities (validate,mutate,generate) under the same consistent YAML syntax, something Gatekeeper also supports but with a different configuration curve for each capability.
Lesson 6 actually installs Kyverno and runs exactly lesson 4's same test — the goal isn't to demonstrate that one "wins," it's for you to see, with real evidence from both engines, that the final result for the user is identical (a Pod with no limits, rejected; a Pod with limits, admitted), even though the internal path to get there is different.
Common mistakes
Assuming Kyverno is "Gatekeeper, but simpler" in every case (oversimplification). What happens: someone, after seeing Kyverno's YAML is shorter than Gatekeeper's Rego for this specific example, concludes Kyverno is always going to be the simpler option. How to spot it: if your conclusion from this lesson is "Kyverno wins, it's easier." How to fix it: for simple structural-matching policies (this module's case), Kyverno does effectively require less code. For policies that need to combine conditions in complex ways — the previous section names it explicitly — Kyverno's syntax starts approaching Rego's complexity, or directly needs additional expressions (JMESPath, CEL) that aren't simpler than Rego, just different.
Thinking you have to pick one of the two forever, across the whole cluster (unnecessary binary decision). What happens: someone assumes a real cluster can only have one policy engine installed at a time. How to spot it: if your plan is "I'm going to decide which one to install and uninstall the other." How to fix it: technically, the two can coexist on the same cluster — this module's lesson 8, in fact, does this on purpose, with both engines active at once over andes-cargo, to honestly document what happens when two guardrails evaluate the same object. In a real team's practice, however, keeping both engines active with overlapping policies adds real operational complexity (two places to check for why something got rejected); most teams do end up standardizing on just one to reduce that load, even though no technical limitation forces it.
Confusing validationFailureAction: Enforce with "this policy is the only one running" (single-field scope). What happens: someone reads Enforce and assumes it's a global Kyverno setting, not that specific ClusterPolicy's. How to spot it: if you expect changing Enforce to Audit in one policy to affect a different ClusterPolicy's behavior. How to fix it: validationFailureAction is a spec field, specific to each ClusterPolicy — every policy you install, present or future, declares its own mode, completely independent of the others.
Exercises
Exercise 1 — Translate, in prose, what this lesson's pattern requires, without looking at the YAML again. Without looking back at this lesson's YAML block, explain in one sentence what exact condition a Pod must meet to pass the Kyverno policy shown here.
See solution
Every container inside the Pod has to declare, simultaneously, four fields with some non-empty value: resources.requests.cpu, resources.requests.memory, resources.limits.cpu, and resources.limits.memory. If any of those four fields is missing a value (or the field doesn't exist at all), the pattern doesn't match the incoming object, and the policy triggers its rejection.
Exercise 2 — Explain, without using the word "better," when a team with prior Terraform/conftest knowledge would choose Gatekeeper over Kyverno. A colleague who already worked through cloud-security-and-guardrails-guide in depth asks which of the two engines to learn first for this module. Without using the word "better" or "worse," what argument would you give them in favor of starting with Gatekeeper?
See solution
A reasonable answer: "If you already wrote Rego rules for conftest, a lot of that knowledge — the declarative language, how to navigate input, the concept of a violation/deny set — is directly reusable in Gatekeeper, with the syntax difference you already saw (input.review.object instead of plain input). Kyverno, on the other hand, asks you to learn a whole new convention — the pattern syntax, its special operators like ?* — even though that convention is, by itself, easier to read for someone with no prior Rego knowledge." The key to a good answer is that it recognizes an advantage of reusing existing knowledge, not an objective technical superiority of one engine over another.
Exercise 3 — Predict what would happen if you tried to require "the memory limit must be double the request" with this lesson's Kyverno pattern. Without researching Kyverno's advanced syntax yet, predict: can the pattern you saw in this lesson (with "?*") directly express that rule?
See solution
No, not directly. The "?*" operator only checks for the presence of a non-empty value — it has no way to compare one field's value against another field's value in the same object (limits.memory against requests.memory with a mathematical relationship between the two). To express that rule, Kyverno would need a more advanced validation syntax than the simple pattern — outside this module's scope — while Rego, being a general-purpose language, can express that comparison with a couple of extra lines within the same rule style you already saw in lesson 3. This is exactly the limit this lesson's "What Rego can do" section described.
Summary and next step
This lesson put, side by side, the same resource-limits policy written in Rego across two objects (Gatekeeper) and in YAML inside just one (Kyverno) — two different languages for the same gatekeeper rule. You saw the three real structural differences between them (number of objects, type of logic, Kyverno's extra capabilities like mutating and generating), and the honest criteria — with no declared winner — that tilt a real team toward one or the other.
Before moving on you should be able to: write from memory the structural difference between a ConstraintTemplate+Constraint and a ClusterPolicy; explain what the "?*" operator means in a Kyverno pattern; and name, without using "better"/"worse," a real criterion that would tilt a team toward each engine.
Next lesson: hands-on — the same policy, in Kyverno. There you're going to actually install Kyverno v1.18.2, apply this lesson's complete ClusterPolicy, and run lesson 4's same test — a Pod that violates it, a Pod that complies — with Kyverno as the deciding engine.
Resources
- Kyverno — Introduction — official documentation, with the project's conceptual comparison between declarative YAML and other policy engines.
- Kyverno — Validate Rules — complete reference for the
patternfield and its operators, including"?*". - Open Policy Agent Gatekeeper — lesson 4's engine, contrasted here point by point.
- Kyverno — Policy Settings — official reference for
validationFailureAction(Enforce/Audit), the conceptual equivalent ofenforcementActionin Gatekeeper.