Skip to content

Cloud Native

Kyverno Explained: Why Modern Platforms Need Policy-as-Code

A practitioner's guide to what Kyverno actually does, what it takes off your platform team's plate, and how its CEL-native CRDs and admission architecture deliver it.

Ivan Porta

Founder & Principal Engineer

8 min read
#kyverno#policy-as-code#kubernetes#admission-control#cel#platform-engineering
Kyverno Explained: Why Modern Platforms Need Policy-as-Code

Kyverno is a CNCF-graduated policy engine that validates, mutates, generates, and cleans up Kubernetes resources with policies written as Kubernetes YAML and CEL, with no new policy language required. Use it to enforce security and operational standards at admission time and in background scans; for a handful of simple validations, Kubernetes’ built-in ValidatingAdmissionPolicy may be enough.

Running pods as root. Deploying images with the latest tag everywhere, unpinned, unsigned, and not traced to a digest. Relying on a Bash script to bootstrap new namespaces with default NetworkPolicies, ResourceQuotas, and pull secrets, all but certain to fail on the next update. Spinning up costly EKS LoadBalancers for demos that linger, forgotten, draining budgets.

Platform teams used to handle this operational overhead manually, and each new team or cluster just added more technical debt. Kyverno helps by bringing all of this under one system.

Kyverno is an open-source, Kubernetes-native policy engine. It lets you write policies in YAML, and now also in CEL, which are checked when resources are created and regularly audited against running workloads. Companies like Coinbase, LinkedIn, and Spotify, as well as the U.S. Department of Defense, use Kyverno in production. Kyverno reached CNCF Graduation at KubeCon Amsterdam in March 2026.

What is Kyverno, actually?

Kyverno is an open-source policy engine for Kubernetes that lets you write policies as Kubernetes resources. Unlike OPA or Gatekeeper, which require you to learn Rego before writing rules, Kyverno uses YAML and supports CEL, so you can work with the same API objects you already know.

Developers or SREs can write a ValidatingPolicy just like they write a Deployment, apply it with kubectl, and see violations as standard Kubernetes events and PolicyReport resources. There’s no need to learn a new language, query model, or way of thinking.

How it works

Kyverno is built on Kubernetes’ dynamic admission controller architecture, with a background scanner layered on top. Policies are evaluated at three points in a resource’s lifecycle:

  1. Admission: When a developer, GitOps tool, or CI/CD pipeline submits a resource, the API server triggers admission webhooks. Kyverno’s webhook checks the request, applies matching policies, and returns allow, deny, or a mutated resource in the request path.

  2. Reconciliation: Background controllers watch for resources that should exist (cleanup policies, generation rules) and reconcile them in a loop, separate from admission.

  3. The new reality: Kyverno’s reporting system regularly scans existing resources against current policies on a schedule, surfacing violations in workloads that were created before a policy was in place or that changed later.

Your application code stays the same. Every resource that goes through the API server is checked, mutated, and recorded.

Policy types

Kyverno’s CEL-native policy types were introduced incrementally, beginning with v1.14 and expanding through v1.17 with the addition of Namespaced versions. This brought the policies.kyverno.io group to a total of eleven CRDs (five cluster-scoped, five namespaced, plus PolicyException). Each type instructs the API server on how to handle the policy outcome:

  • ValidatingPolicy: Validates resources against a CEL expression.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-team-label
spec:
  validationActions: [Deny]
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
      message: "Pods must carry a 'team' label."
  • MutatingPolicy: Mutates resources during admission using CEL-based applyConfiguration or jsonPatch expressions, similar to Kubernetes MutatingAdmissionPolicy.
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: default-run-as-non-root
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: >
          Object{
            spec: Object.spec{
              securityContext: Object.spec.securityContext{
                runAsNonRoot: true
              }
            }
          }
  • GeneratingPolicy: Creates downstream resources when a trigger resource is created, namespace defaults, network policies, or RBAC bindings.
apiVersion: policies.kyverno.io/v1
kind: GeneratingPolicy
metadata:
  name: default-deny-on-namespace
spec:
  evaluation:
    synchronize:
      enabled: true
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["namespaces"]
  variables:
    - name: targetNs
      expression: "object.metadata.name"
    - name: downstream
      expression: >-
        [
          {
            "kind": dyn("NetworkPolicy"),
            "apiVersion": dyn("networking.k8s.io/v1"),
            "metadata": dyn({
              "name": "default-deny"
            }),
            "spec": dyn({
              "podSelector": dyn({}),
              "policyTypes": dyn(["Ingress", "Egress"])
            })
          }
        ]
  generate:
    - expression: generator.Apply(variables.targetNs, variables.downstream)
  • ImageValidatingPolicy: Checks container images against trusted attestors like Cosign, Notary, or sigstore TUF, and supports in-toto attestations. SBOMs are also supported as one type of attestation payload.
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
  name: require-signed-images
spec:
  validationActions: [Deny]
  webhookConfiguration:
    timeoutSeconds: 30
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  matchImageReferences:
    - glob: "ghcr.io/kyverno/test-verify-image*"
  attestors:
    - name: kyvernoCosign
      cosign:
        key:
          data: |
            -----BEGIN PUBLIC KEY-----
            MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nXRh950IZbRj8Ra/N9sbqOPZrfM
            5/KAQN0/KjHcorm/J5yctVd7iEcnessRQjU917hmKO6JWVGHpDguIyakZA==
            -----END PUBLIC KEY-----
  validations:
    - expression: >-
        images.containers.map(image, verifyImageSignatures(image, [attestors.kyvernoCosign])).all(e, e > 0)
      message: "Images from ghcr.io/kyverno/test-verify-image must be signed by the Kyverno project key."
  • DeletingPolicy: Removes resources that match a selector on a set schedule. This is helpful for temporary environments and cleanup tasks.
apiVersion: policies.kyverno.io/v1
kind: DeletingPolicy
metadata:
  name: cleanup-stale-ephemeral-pods
spec:
  schedule: "0 2 * * *"
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["*"]
        resources: ["pods"]
        scope: "Namespaced"
    namespaceSelector:
      matchLabels:
        environment: test
  conditions:
    - name: olderThan48h
      expression: "time.now() - timestamp(object.metadata.creationTimestamp) > duration('48h')"
    - name: ephemeralOrFinished
      expression: >-
        (has(object.metadata.labels) && 'ephemeral' in object.metadata.labels && object.metadata.labels.ephemeral == 'true') || (has(object.status) && has(object.status.phase) && (object.status.phase == 'Succeeded' || object.status.phase == 'Failed'))

The Namespaced versions (NamespacedValidatingPolicy, NamespacedMutatingPolicy, NamespacedGeneratingPolicy, NamespacedImageValidatingPolicy, NamespacedDeletingPolicy) are important for multi-tenancy. Teams can create and manage policies in their own namespaces without needing cluster-admin rights, while platform owners use the cluster-scoped types for policies that must apply everywhere.

These CEL-based policies are about as mature as the ValidatingAdmissionPolicy in Kubernetes, with the API server handling more of the work. Kyverno adds features like a policy engine, reporting, exceptions, generation, and image validation, which VAP does not include.

The older ClusterPolicy and CleanupPolicy types still work for now, but they are being phased out. Kyverno 1.17 marked them as deprecated, and they will be removed later in 2026. Plan to migrate to the CEL-native CRDs soon, and write new policies using these types unless you have a specific reason not to.

When should you use ValidatingAdmissionPolicy instead?

Kubernetes 1.30 and later includes ValidatingAdmissionPolicy as a built-in CEL validator that doesn’t need a separate controller. For some teams, this is enough, but for most, it isn’t.

Primary functionSynchronous validation at admissionValidation, mutation, generation, deletion
Enforcement pointsAdmission onlyAdmission, background scans, pipelines, CLI
PayloadsKubernetes objectsKubernetes objects, plus any JSON/YAML
CEL libraryBasicExtended (HTTP calls, image verification, Kubernetes lookups)
External dataKubernetes resources or HTTP calls
Policy bindingsManualAutomatic
Background scansPeriodic, plus on policy change
Reporting & auditPolicyReport + EphemeralReport CRDs
ExceptionsFine-grained PolicyException
Image signature verification
Auto-generationPod controllers, ValidatingAdmissionPolicy
TestingKyverno CLI (unit), Chainsaw (e2e)
ArchitectureBuilt into the API server (no controller)Requires Kyverno controller

If you only need to validate resources at admission time, VAP is a lighter option and is the right choice. If you need more features, you’ll want a policy engine, and Kyverno offers the closest match between writing policies in YAML and treating them as code.

Operational reality

  • Rollout, not install. Starting Kyverno in audit mode is recommended. Use validationActions: [Audit] for CEL-native types or failureAction: Audit for legacy ClusterPolicy (legacy Enforce corresponds to Deny). Avoid deploying deny policies immediately, as this can disrupt controllers if their ServiceAccounts do not meet label requirements. Run in audit mode for at least a week, review PolicyReport resources, address violations, and then switch to deny.

  • Mutation is not a security control. MutatingPolicy works well for setting defaults, image pull secrets, resource requests, and standard labels, but it’s not a replacement for validation. A user who can submit a pod spec might submit one that your mutation never processes, due to admission order or exemptions. Anything that must always be true should use a ValidatingPolicy.

A practical recommendation

If you are evaluating a policy engine for a Kubernetes platform and you do not already have a strong reason to pick OPA/Gatekeeper, pilot Kyverno first. The install fits in an afternoon. If it does what you need, you just saved a quarter of operational work. If it doesn't, you have a clear, documented reason to go somewhere else.

Frequently asked

Three questions we keep getting about Kyverno.

What is Kyverno, and where does it sit in Kubernetes admission flow?

Kyverno is a Kubernetes-native policy engine that runs as an admission controller, intercepting every API request via webhooks to validate, mutate, or generate resources before they are persisted.

Should I start with legacy ClusterPolicy or the newer CEL-based policy types?

Use the CEL-based types (ValidatingPolicy, MutatingPolicy, GeneratingPolicy, ImageValidatingPolicy) for new work. ClusterPolicy still works but is deprecated as of Kyverno 1.17.

How do Audit and Enforce differ, and what do background scans do?

Deny (called Enforce in legacy ClusterPolicy) rejects failing requests at admission; Audit admits them but records the failure. Background scans re-evaluate existing resources on a schedule and write results to EphemeralReport intermediaries, which Kyverno's reports controller aggregates into the PolicyReport resources operators consume.

This site uses cookies for analytics.