Tools Used in DevSecOps · Kyverno

Kyverno

Kyverno is a policy engine that runs entirely inside Kubernetes, behind an admission webhook, and its single defining decision is what language a policy gets written in: not Rego, not a bespoke DSL, but the same declarative YAML you already write for a Deployment or a NetworkPolicy. A rule that says "containers must run as non-root" looks like a fragment lifted straight out of a Pod spec, because that's almost literally what it is. That one choice is why Kyverno shows up on so many clusters run by teams who never signed up to become policy-language specialists: its four rule types — validate, mutate, generate, and verifyImages — cover rejecting a bad resource, silently fixing a missing field, auto-provisioning a companion resource like a default-deny NetworkPolicy, and checking a container image's cryptographic signature, and every one of them is expressed the same way: as a pattern laid directly over the resource it's judging.

☺ Explain it like I'm 10

Picture two hall monitors checking notes for a hall pass. The first monitor reads notes in the exact same handwriting every student already uses for everything else — if you can fill out a form, you can write a note this monitor accepts. The second monitor is just as capable, but insists every note first be translated into a private shorthand only they use, and that shorthand takes real practice before your first note is ever accepted. Kyverno is the first monitor: its policies are Kubernetes YAML, so anyone who can already read a Deployment can read — and mostly write — a Kyverno policy. Its main rival, OPA Gatekeeper, is closer to the second: powerful in the hands of someone fluent in its shorthand, a language called Rego, but that fluency is exactly what most Kubernetes-focused teams don't already have.

🤖Your host for this topic: Recon the Robot — the reconciliation loop from IaC security & policy as code doesn't stop at a Terraform plan. Kyverno is the exact same discipline — a rule, checked automatically, every single time — applied one layer down, at the Kubernetes admission boundary, in a language that reads like the manifests it's reconciling.

What Kyverno is and the problem it solves

☺ Like you're 10: It's a policy checker that lives inside the cluster itself, and its policies are ordinary Kubernetes objects written in ordinary Kubernetes YAML, not files in some other language somewhere else.

Kyverno began as an internal project at Nirmata and was open-sourced and donated to the Cloud Native Computing Foundation in 2020; it's a CNCF Incubating project as of this writing — check the CNCF landscape for its current maturity level before citing that in anything official. Functionally it's a Kubernetes-native admission controller: install it, typically via its Helm chart, and it registers mutating and validating webhooks with the API server. Everything past that point is expressed as ordinary Kubernetes custom resources. A ClusterPolicy (cluster-scoped) or Policy (namespace-scoped) isn't a config file sitting in a repository that Kyverno reads on startup — it's a live object you kubectl apply and kubectl get exactly like a Deployment, versioned in git the same way any manifest is, and inspectable with the same tool everyone on the team already has open.

It's worth being precise about where Kyverno sits in the pipeline, because two other pages already cover policy engines and it's easy to conflate them. Checkov, tfsec, and OPA & Conftest check a Terraform plan before anything is ever created — the resource doesn't exist yet, so a violation is caught as a text diff in a pull request, the pre-deploy layer IaC security & policy as code and Infrastructure as Code Hardening cover in depth. Kyverno checks a live Kubernetes API request at the moment a resource is actually being created or changed inside the cluster — after the plan already applied, at the one chokepoint every workload has to pass through to run at all, whether it arrived from a pristine CI pipeline or a kubectl apply someone ran by hand from their laptop. Neither checkpoint substitutes for the other, which is exactly the argument Platform Engineering for Security Guardrails makes about admission control as the layer a per-repo pipeline check can't be silently skipped at.

Four rule types cover essentially everything a real cluster asks of a policy engine, and the rest of this page takes each one in turn: validate accepts or rejects a resource against a pattern, mutate silently patches a resource to add or correct a field, generate creates — and optionally keeps in sync — a companion resource whenever a trigger resource appears, and verifyImages checks a container image's cryptographic signature before letting its Pod through.

Architecture: policies are live Kubernetes objects, not config files

☺ Like you're 10: Kyverno isn't one program — it's a small team of controllers sharing the same policy objects: one blocks or fixes things as they happen, one re-checks things that already exist, one writes down what it found, and one tidies up on a schedule.

Kyverno versions from roughly the 1.10 release line onward split what used to be a single deployment into four narrower controllers, each with a smaller blast radius if it misbehaves: the admission controller (kyverno) runs the actual mutating and validating webhooks and is the only one in the synchronous, request-blocking path; the background controller periodically re-evaluates already-existing resources against every policy and reconciles generate rules whose source resource changed after the fact; the reports controller aggregates the background controller's findings into PolicyReport and ClusterPolicyReport objects; and the cleanup controller runs scheduled CleanupPolicy/ClusterCleanupPolicy objects — a TTL-style mechanism for deleting resources that match a condition, useful for things like expired PolicyException waivers. Older or minimal installs may still run these as one deployment; the logical split is the part worth remembering.

Three fields on every policy's spec decide how much of that machinery actually engages, and getting them wrong is the single most common source of "why didn't this policy do anything" tickets:

kubectl apply / any write → Kubernetes API server Admission webhook mutating, then validating SYNCHRONOUS — can block Background controller scheduled re-scan of resources already live Kyverno rule engine validate — allow or deny mutate — patch the object generate — create/sync verifyImages — check signature one ClusterPolicy/Policy object can hold rules of every type JMESPath variables via {{ }} Admission response allow (optionally patched) or deny + failing rule message the only outcome that blocks PolicyReport / ClusterPolicyReport kubectl get policyreport -A visibility only — never blocks Same engine, same policy objects either way — only the webhook path can stop a request before it happens.

The four rule types: validate, mutate, generate, verifyImages

☺ Like you're 10: One rule type says no, one quietly fixes what's missing, one creates a second object to go with the first, and one checks a signature — and all four are written the same way, as a pattern laid over the resource.

Every rule lives inside a policy's rules list and starts with a match block selecting which resources it applies to; from there, the rule body is one of the four types below. Kyverno's pattern language layers a handful of anchors on top of plain YAML: * and ? are wildcards for matching part of a value, =(key) is a conditional anchor that only checks the field if it's present (rather than failing when it's absent entirely), ^(key) requires the field to exist, and +(key) — used in mutate rules specifically — means "add this value only if the field isn't already set." Variables come from JMESPath expressions wrapped in {{ }}, pulling from context like request.object, the resource that triggered the rule.

validate — accept or reject against a pattern

The most common rule type. A validate.pattern block mirrors the shape of the resource it's checking, and Kyverno walks the real object looking for a mismatch:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-non-root
spec:
  validationFailureAction: Audit   # flip to Enforce once the PolicyReport backlog is clean — see Gotchas
  background: true
  rules:
    - name: containers-must-run-as-non-root
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "containers must set securityContext.runAsNonRoot: true"
        pattern:
          spec:
            =(securityContext):
              runAsNonRoot: true
            containers:
              - =(securityContext):
                  runAsNonRoot: true

The =() anchors mean this rule doesn't fail a Pod that sets runAsNonRoot only at the Pod level and not per-container, or vice versa — either placement satisfies it, but if neither is set at all, the rule fails, because at least one =() block has to actually match something. Getting comfortable with that anchor is most of learning to write Kyverno validate rules that don't produce false positives on legitimate variation.

mutate — patch a resource instead of just rejecting it

Where OPA-style engines can only say yes or no, Kyverno can fix the problem itself before the object is ever persisted. A patchStrategicMerge block overlays new content onto matching resources, and the +() anchor is what keeps it from clobbering a value someone already set on purpose:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: default-resource-requests
spec:
  rules:
    - name: add-default-requests
      match:
        any:
          - resources:
              kinds: [Pod]
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - (name): "*"          # matches every container in the array
                resources:
                  +(requests):        # only ADD requests if this container doesn't already set them
                    cpu: 100m
                    memory: 128Mi

Mutation always runs before validation in the admission chain, which is deliberate: a Pod this rule patches with default requests can then pass a separate validate rule that requires requests to be set, without a developer having to write the field themselves. That ordering is also the thing to watch — a mutate rule that's silently "fixing" something can mask a gap the developer never learns about, which is why pairing a mutate rule with visibility (a linter, a PolicyReport, a code-review norm) matters as much as writing the rule itself.

generate — provision a companion resource automatically

A generate rule fires when its trigger resource is created and produces a second, related resource — most commonly a default-deny NetworkPolicy for every new namespace, closing exactly the gap Kubernetes Security Deep Dive covers about NetworkPolicy rules being additive and denying by default once any policy exists:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-deny-networkpolicy
spec:
  rules:
    - name: default-deny-all
      match:
        any:
          - resources:
              kinds: [Namespace]
      exclude:
        any:
          - resources:
              namespaces: [kube-system, kyverno]
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-all
        namespace: "{{request.object.metadata.name}}"
        synchronize: true        # if someone deletes or edits it, the background controller reverts it
        data:
          spec:
            podSelector: {}
            policyTypes: [Ingress, Egress]

synchronize: true is the field that turns this from a one-time stamp into an ongoing guarantee: every new namespace gets the NetworkPolicy the moment it's created, and if someone deletes it later — accidentally, or to "just get this working" during an incident — the background controller puts it back on its next pass. That's the same never-negotiates-with-drift behavior IaC security & policy as code describes for infrastructure, running here against live cluster objects instead of cloud resources.

verifyImages — check a container's cryptographic signature

This rule type is Kyverno's most turnkey answer to supply-chain trust: it verifies a Pod's images against a Sigstore cosign signature before admitting it, using either a fixed public key or "keyless" verification tied to an OIDC identity — the same mechanism Platform Engineering for Security Guardrails and Secure SDLC Gates & the DevSecOps Maturity Model both close their build-stage signing story with:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources: { kinds: [Pod] }
      verifyImages:
        - imageReferences: ["registry.acme.internal/*"]
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/acme-corp/*/.github/workflows/*"
                    issuer: "https://token.actions.githubusercontent.com"

Read Sigstore & cosign for what "keyless" signing is actually verifying under the hood — the subject and issuer fields above are checking a specific CI workflow's OIDC identity, not a long-lived private key. This rule is deliberately scoped to registry.acme.internal/* rather than every image on the cluster, because a hard requirement that every image everywhere carry a signature would also block every base image, sidecar, and vendor chart the cluster legitimately runs unsigned — imageReferences is how a verifyImages rule stays a gate on your own supply chain instead of an accidental gate on the entire ecosystem you depend on.

◆ Key idea

validate, mutate, and generate aren't three separate tools bolted together — they're the same pattern-matching engine pointed at three different jobs: reject, fix, and provision. A team that's only ever used Kyverno for validate is using a quarter of what a single ClusterPolicy object can actually do, and the other three rule types are exactly the gap a Terraform-plan-time scanner or a pure validating engine like OPA Gatekeeper's classic Constraint model can't close on its own.

Autogen: one policy, every pod controller

☺ Like you're 10: Write a rule about Pods once, and Kyverno automatically copies it to also cover Deployments, StatefulSets, Jobs, and everything else that eventually creates Pods — so you don't have to write the same rule six times.

Almost nobody creates a bare Pod directly in a real cluster — they create a Deployment, a StatefulSet, a DaemonSet, a Job, or a CronJob, and that controller creates the Pod. A validate or mutate rule that only matched kind: Pod would therefore miss the actual moment of creation entirely, since a Deployment's own admission request never has kind: Pod on it. Kyverno solves this with autogen: when a rule's match block targets Pod, Kyverno automatically generates equivalent rules — named autogen-<rule-name> — that fire on the parent controller kinds too, rewriting the pattern to match each controller's spec.template.spec path automatically. You write one rule; six show up in kubectl describe cpol.

This is almost always what you want, and it's also the single most common source of "why did my Deployment get rejected when my policy only mentions Pod" confusion — the answer is that it did mention Pod, just not directly. Autogen behavior is controllable with the pod-policies.kyverno.io/autogen-controllers annotation on the policy: set it to a specific comma-separated list (Deployment,StatefulSet) to narrow which controllers get the generated rules, or to none to disable autogen entirely and match controllers explicitly yourself in the original rule.

Background scanning and PolicyReports: visibility before enforcement

☺ Like you're 10: Before you lock the door, it helps to know how many people are already inside — background scanning checks what's already running against a new rule and writes down what it finds, without kicking anyone out yet.

A brand-new policy applied with validationFailureAction: Enforce only ever sees resources created or changed after it exists — it says nothing about the two hundred Pods already running that would have failed it. The background controller closes that gap: on a schedule, it re-evaluates every existing matching resource against every policy with background: true, regardless of what the policy's failure action is, and writes the results to PolicyReport (namespaced) and ClusterPolicyReport (cluster-scoped) objects — a reporting format shared across several Kubernetes policy engines, not unique to Kyverno, which is why kubectl get policyreport -A is a genuinely useful "what's out of compliance right now" query independent of which tool produced the finding.

The practical workflow this enables is the same audit-then-enforce rollout IaC security & policy as code recommends for Terraform policy: ship a new rule as validationFailureAction: Audit, let the background controller run for a few days, review the PolicyReport backlog it produces, fix or explicitly except the pre-existing violations, and only then flip the rule to Enforce. Skipping straight to Enforce on a rule nobody's tuned against real cluster traffic doesn't just risk one bad admission decision — it can turn into an outage the moment a routine Deployment rollout tries to create a Pod that's been silently non-compliant for months. Part 5 of the capstone and the policy-as-code drill both walk this exact rollout end to end.

Day-to-day: installing, writing, testing, and applying policies

☺ Like you're 10: Install it once with Helm, then almost everything else is either kubectl on the policy objects themselves, or the kyverno CLI checking a policy before it ever touches a real cluster.

# install — Kyverno ships as a Helm chart, same as most platform components
$ helm repo add kyverno https://kyverno.github.io/kyverno/
$ helm repo update
$ helm install kyverno kyverno/kyverno -n kyverno --create-namespace

# every cluster-scoped and namespaced policy currently loaded — cpol/pol are the short names
$ kubectl get cpol
$ kubectl get pol -A
$ kubectl describe cpol require-non-root        # autogen-* rules show up here too

# visibility from background scans — this is where Audit-mode findings live
$ kubectl get policyreport -A
$ kubectl get clusterpolicyreport

# local, no-cluster-needed policy testing with the kyverno CLI
$ kyverno apply require-non-root.yaml --resource pod.yaml   # dry run against one file, no cluster contact
$ kyverno apply require-non-root.yaml --cluster              # dry run against everything live right now
$ kyverno test .                                              # run a declared kyverno-test.yaml test suite

kyverno test is worth calling out on its own — it's what makes a policy itself reviewable and CI-testable, not just the resources it checks, the same discipline OPA & Conftest gets from opa test against Rego. A test file declares which policies and resources to load, then asserts the expected pass/fail outcome for each policy-rule-resource combination:

# kyverno-test.yaml
name: require-non-root-test
policies:
  - require-non-root.yaml
resources:
  - resources.yaml       # contains both a bad-pod and a good-pod object, by metadata.name
results:
  - policy: require-non-root
    rule: containers-must-run-as-non-root
    resource: bad-pod
    kind: Pod
    result: fail
  - policy: require-non-root
    rule: containers-must-run-as-non-root
    resource: good-pod
    kind: Pod
    result: pass

Wiring kyverno test . into the pipeline that manages the policy repository itself closes a loop worth noticing: the policies enforcing shift-left on application code get their own shift-left check before they're ever applied to a cluster — a broken pattern that would have silently matched nothing, or a rule that's accidentally too strict for a legitimate workload, gets caught in the policy repo's own pull request instead of in a production admission failure at 2am.

Gotchas and failure modes

☺ Like you're 10: Most surprises come from one of a handful of habits: forgetting autogen exists, forgetting Audit mode doesn't block anything, or forgetting that the webhook itself is a thing that can go down.

The fix for that synchronize: true gotcha is Kyverno's PolicyException resource — a tracked, reviewable Kubernetes object, not a Slack message nobody can find again six months later, and scoped narrowly to exactly the policy, rule, and namespace it exempts:

apiVersion: kyverno.io/v2beta1
kind: PolicyException
metadata:
  name: legacy-billing-non-root-exception
  namespace: legacy-billing
spec:
  exceptions:
    - policyName: require-non-root
      ruleNames:
        - containers-must-run-as-non-root
  match:
    any:
      - resources:
          kinds: [Pod]
          namespaces: [legacy-billing]
⚠ Watch out — rolling straight to Enforce on an untuned rule

The failure mode isn't hypothetical: a team writes a validate rule, feels confident because it reads clearly, sets validationFailureAction: Enforce immediately, and ships it. The very next routine Deployment rollout — not a new feature, just a normal image-tag bump on an existing service that was already quietly non-compliant — fails admission, and the rollout is stuck. The rule wasn't wrong; it just never got the Audit-mode period this course keeps recommending, where a background scan would have surfaced the existing violation before it became an incident instead of during one.

Kyverno vs. OPA Gatekeeper: same job, different language

☺ Like you're 10: Both tools sit at the same door checking the same kinds of things — the real difference is whether you have to learn a whole new language to write the rule, or whether you already basically know it.

OPA & Conftest covers Rego in full; the short version needed here is that Gatekeeper is the Kubernetes admission controller built on the Open Policy Agent engine, wrapping Rego policies behind a validating — and, more recently, mutating — webhook, with policies expressed as ConstraintTemplate CRDs (compiling the Rego itself) parameterized by separate Constraint CRDs. Functionally, Kyverno and Gatekeeper solve the identical admission-control problem: both register webhooks, both evaluate a resource against a policy, both can allow or deny. The entire practical difference is what you have to learn to write the rule, and that difference is larger than it might sound.

Put the same requirement side by side — every container must declare resource requests and limits, the exact rule OPA & Conftest writes in Rego for the same reason this page writes it in Kyverno:

# Gatekeeper / Rego — requires understanding partial-set iteration (some x in ...),
# and the classic "undefined isn't false" trap if the field is simply absent.
package k8srequiredresources

violation[{"msg": msg}] {
  container := input.review.object.spec.containers[_]
  not container.resources.limits
  msg := sprintf("container %v has no resource limits set", [container.name])
}
# Kyverno — reads like the Pod spec it's checking, because it basically is one.
validate:
  message: "every container must set resources.requests and resources.limits"
  pattern:
    spec:
      containers:
        - resources:
            requests: { memory: "?*", cpu: "?*" }
            limits: { memory: "?*", cpu: "?*" }

The Rego version isn't wrong or badly written — it's idiomatic Rego, and an engineer fluent in the language will read it just as fast as the YAML. The point is exactly that clause: fluent in the language. The Rego version depends on understanding partial-set unification (container := input...[_] iterating an array), negation over a possibly-absent field, and the specific shape Gatekeeper injects into input.review.object — none of which is knowledge a Kubernetes-fluent engineer already has by default. The Kyverno version depends on knowing that "?*" means "must be present and non-empty," a single convention learnable in one sitting, applied to a structure — a Pod's containers array — the same engineer has already read a hundred times.

KyvernoOPA GatekeeperValidatingAdmissionPolicy
Policy languageNative Kubernetes YAML (pattern overlays + JMESPath); newer CEL-based policy kinds are emerging alongside itRego — a real declarative logic language, genuinely new to most Kubernetes engineersCEL, built directly into the policy object
Onboarding cost for a K8s-fluent teamLow — a rule reads like the manifest it governsReal — Rego's evaluation model (unification, sets, undefined-vs-false) is its own skill, measured in days to weeks even for strong engineersLow for simple rules; CEL syntax is still new to most, but far smaller than a full language
MutationNative — mutate rules, first-class from the startSupported, added later as separate Assign/AssignMetadata CRDs alongside the original validating-only modelNo — validating only, by design
Generate companion resourcesNative — generate rules, with optional synchronizeNo native equivalentNo
Image signature verificationNative — verifyImages, built-in cosign integrationNo native equivalent — needs external tooling (Sigstore's Policy Controller, or hand-rolled Rego plus a lookup) wired in separatelyNo
Needs a running webhook PodYesYesNo — compiled directly into the apiserver
Reusable outside KubernetesNo — Kubernetes-only by designYes — the same Rego engine (via plain OPA and Conftest) evaluates Terraform plans, Dockerfiles, and live authorization requests with the identical languageNo

None of this makes Gatekeeper the wrong choice — it makes it a different trade. An organization that already runs Rego for Terraform policy via OPA & Conftest, or that needs genuinely complex cross-resource logic Kyverno's pattern language strains to express, gets real value from one policy language spanning every layer instead of two. But for the far more common case — a platform or DevOps team that's fluent in Kubernetes manifests and wants admission control, mutation, resource generation, and image verification without adding a new language to the team's skill inventory — Kyverno's lower learning curve isn't a minor convenience. It's the difference between a policy a second engineer can review and confidently modify next quarter, and one that quietly becomes "the Rego file nobody but its original author fully trusts themselves to change."

🎬 At the Shift-Left Squad
🤖

Recon the Robot: Deployment rejected. checkout-api — no runAsNonRoot anywhere in the spec.

🦫

Benny the Beaver: I wrote a Deployment, Recon, not a Pod. My policy YAML clearly only says kind: Pod.

🤖

Recon the Robot: It still only says Pod. Autogen copied it onto Deployment, StatefulSet, and four other controller kinds the moment you applied it — kubectl describe cpol shows the generated rule with autogen- in front of its name.

🦝

Rocky the Raccoon: So if I skip the Deployment and apply a bare Pod directly, does the rule still catch me?

🤖

Recon the Robot: The original rule already matches Pod directly — autogen adds coverage, it doesn't remove any. There's no gap there to find.

🐢

Timmy the Turtle: Was this rule even in Audit mode first? Or did it go straight to Enforce on a service nobody checked against it yet?

🦫

Benny the Beaver: ...Enforce. On the first try.

🐢

Timmy the Turtle: Then the policy did its job and you skipped a step. Audit first, read the PolicyReport, fix what's already broken, then Enforce.

🐦

Pip the Hummingbird: While you're both in here — I still don't see a signature-verification rule on this cluster at all. Every image just walks straight past that door.

🤖

Recon the Robot: Noted. Same policy object, one more rule type, same YAML. That's next.

✓ Checkpoint

1. Name Kyverno's four rule types in one clause each, and say which one runs first in the admission chain relative to the others. 2. What does validationFailureAction: Audit actually do — and not do — and why does this course recommend shipping a new rule that way before switching to Enforce? 3. A validate policy's match block only lists kind: Pod, yet a Deployment still gets rejected by it. Explain what happened and where you'd confirm it. 4. Why is a webhook-based engine's failurePolicy setting a genuine trade-off rather than something with an obviously correct default? 5. Rewrite, in one sentence, why a Kubernetes-fluent platform team might choose Kyverno over OPA Gatekeeper for the same admission-control job — and name one situation where Gatekeeper's Rego is still the better trade despite the steeper learning curve.

Check your answers
  1. validate accepts or rejects a resource against a pattern; mutate patches a resource to add or fix a field; generate creates (and optionally keeps synchronized) a companion resource; verifyImages checks a container image's cryptographic signature. Mutation runs before validation in the admission chain, so a mutate rule can fix a field that a validate rule then checks.
  2. Audit evaluates the policy and records findings in a PolicyReport/ClusterPolicyReport — it does not block anything. It's recommended first because it surfaces how many existing resources would fail the rule before that failure becomes an admission-time outage; flipping straight to Enforce on an untuned rule risks blocking a routine, unrelated deployment the moment it touches an already-non-compliant resource.
  3. Kyverno's autogen feature automatically generates equivalent rules — named autogen-<rule-name> — for pod-controller kinds like Deployment, StatefulSet, and Job whenever a rule's match block targets Pod, since those controllers are what actually create the Pod in practice. Confirm it with kubectl describe cpol <policy-name>, which lists the generated rules alongside the original.
  4. Because there's no failure mode that's free: failurePolicy: Fail means a down or unreachable Kyverno webhook blocks every matching admission request cluster-wide until it recovers, while failurePolicy: Ignore avoids that outage but silently admits everything unchecked while Kyverno is unavailable. Which trade-off is acceptable depends on how security-critical the specific rule is and how reliable the webhook deployment itself is.
  5. Kyverno lets a team already fluent in Kubernetes manifests write, review, and maintain policy without learning a new language, since its policies read like the resources they govern — Rego, by contrast, is a genuinely new declarative logic language with its own real onboarding cost. Gatekeeper's Rego still wins when policy logic needs to be genuinely complex or cross-resource, or when the same policy language needs to be reused outside Kubernetes entirely — for Terraform plans, Dockerfiles, or live authorization decisions, the domain OPA & Conftest covers and Kyverno was never built to reach.