DEV Community

Indra Gusti Prasetya
Indra Gusti Prasetya

Posted on • Originally published at indragustiprasetya.com

Block Privileged Pods with a Kyverno ValidatingPolicy

You will end up with one stable Kyverno ValidatingPolicy that denies any pod carrying a privileged: true container, written against the API that replaced ClusterPolicy in 1.18. The catch that makes this worth a walkthrough: copy an old example onto a current cluster and it can apply cleanly while enforcing nothing.

Every blog, and every AI assistant, hands you a Kyverno ClusterPolicy with validationFailureAction: Enforce. On a current cluster that answer is wrong in three quiet ways. ClusterPolicy was deprecated in Kyverno 1.17 (announced February 2026) with removal already scheduled. The replacement ValidatingPolicy graduated to stable in Kyverno 1.18 (announced May 5, 2026 on the CNCF blog). And the field that turns enforcement on got renamed. Paste a stale snippet onto 1.18 and, in the worst case, it applies with no error and blocks nothing.

Admission control like this stops the object from ever being created. If you also want to kill a privileged process at the kernel after something slips past policy, that is Tetragon's runtime enforcement job, a separate layer. Think of the policy below as a companion to defenses like running pods in user namespaces with hostUsers: false, not a replacement for them.

This tutorial builds the correct thing: a stable ValidatingPolicy (API policies.kyverno.io/v1) that denies privileged pods, covers init and ephemeral containers, and survives a container with no securityContext at all. That null case is the one that makes the "obvious" CEL expression throw or silently pass. It targets platform and security engineers running Kyverno on Kubernetes 1.30+.

Prerequisites

  • A Kubernetes cluster on 1.30 or newer. The ValidatingPolicy CEL engine builds on the upstream ValidatingAdmissionPolicy machinery that stabilized in 1.30.
  • Kyverno 1.18+ installed. Older releases only expose ValidatingPolicy under policies.kyverno.io/v1alpha1; the v1 version arrives in 1.18.
  • kubectl with admin rights, and optionally the Kyverno CLI (kyverno) for offline or CI testing.
  • Cluster admin. You are registering an admission policy that affects every namespace you match.

Step-by-step

1. Install (or confirm) Kyverno 1.18+

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
Enter fullscreen mode Exit fullscreen mode

Now confirm you actually got a 1.18+ image. The app version, not the chart version, is what matters here:

kubectl get deploy -n kyverno kyverno-admission-controller \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
Enter fullscreen mode Exit fullscreen mode

You need a tag of v1.18.x or later. Anything older and the policies.kyverno.io/v1 API below simply won't exist.

2. Confirm the ValidatingPolicy CRD is actually installed

This is the step everyone skips, and it is the cause of the most-Googled failure for this feature. Per Kyverno issue #12441, installing the Helm chart with default CRD settings does not always create the ValidatingPolicy CRD:

kubectl get crd | grep -E 'validatingpolicies.policies.kyverno.io'
Enter fullscreen mode Exit fullscreen mode

If that returns nothing, your kubectl apply in step 4 fails with no matches for kind "ValidatingPolicy" (see Common pitfalls). Reinstall with the policy CRDs enabled in the chart's CRD values before continuing. Do not downgrade the API version to make the error go away.

3. Write the ValidatingPolicy

# disallow-privileged.yaml
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: disallow-privileged-containers
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["pods"]
  variables:
    - name: allContainers
      expression: >-
        object.spec.containers +
        object.spec.?initContainers.orValue([]) +
        object.spec.?ephemeralContainers.orValue([])
  validations:
    - expression: >-
        variables.allContainers.all(c,
          c.?securityContext.?privileged.orValue(false) == false)
      message: "Privileged mode is disallowed. Every container must set securityContext.privileged to false or leave it unset."
Enter fullscreen mode Exit fullscreen mode

Four things here are the whole point of the article.

kind: ValidatingPolicy, not ClusterPolicy. The new type is a superset of the upstream Kubernetes ValidatingAdmissionPolicy, which is why it uses matchConstraints.resourceRules (Kubernetes' shape) instead of Kyverno's old match.any.resources.kinds.

validationActions: [Deny], not validationFailureAction: Enforce. Different field, different value. Deny blocks the request at admission; Audit and Warn are the non-blocking modes. The base policy in Kyverno's library ships as Audit, so change it to Deny or it enforces nothing. This one rename is why so many copied examples pass review and protect nothing.

CEL optional chaining (.? plus .orValue(false)). A container with no securityContext has no privileged field. The naive c.securityContext.privileged == false throws a CEL evaluation error on those pods; c.?securityContext.?privileged.orValue(false) resolves the missing path safely to false. This mirrors the exact expression Kyverno ships in its pod-security-vpol baseline policy.

allContainers merges init and ephemeral containers. Check only spec.containers and a privileged initContainer walks straight in. It is the single most common miss in hand-rolled pod-security rules, and it matters because a privileged container is the shortest path to a container escape.

4. Apply it

kubectl apply -f disallow-privileged.yaml
kubectl get validatingpolicy disallow-privileged-containers
Enter fullscreen mode Exit fullscreen mode

The policy is cluster-scoped and takes effect on the next matching admission request. No restart needed.

5. (Optional) Gate it in CI before it ever reaches the cluster

kyverno apply disallow-privileged.yaml --resource privileged-test.yaml
Enter fullscreen mode Exit fullscreen mode

The Kyverno CLI evaluates the same policy offline, so you can fail a pull request on a privileged manifest instead of discovering it at deploy time.

Verify it works

Try to create a privileged pod:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: privileged-test
spec:
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        privileged: true
EOF
Enter fullscreen mode Exit fullscreen mode

Expected output, the request rejected at admission:

Error from server: error when creating "STDIN": admission webhook
"vpol.validate.kyverno.svc-fail" denied the request:
Privileged mode is disallowed. Every container must set
securityContext.privileged to false or leave it unset.
Enter fullscreen mode Exit fullscreen mode

Now flip privileged: true to false (or delete the securityContext block) and re-apply. The pod should create normally. Run both halves. The bad manifest blocked and the good one admitted is what proves the Deny action and the null-safe expression are each doing their job. One check without the other tells you nothing.

Common pitfalls

  • no matches for kind "ValidatingPolicy". The full error reads error: resource mapping not found for name: "disallow-privileged-containers" ... no matches for kind "ValidatingPolicy" in version "policies.kyverno.io/v1". The CRD isn't installed (Kyverno issue #12441) or you're on Kyverno < 1.18. Fix it with step 2, not by blindly changing the API version.
  • Copying v1alpha1 from the policy library. Kyverno's own published pod-security-vpol pages still carry apiVersion: policies.kyverno.io/v1alpha1 and minversion: 1.14.0. On a 1.18 cluster prefer v1. The v1alpha1 alias may still resolve, but new manifests should target the graduated version so they don't break when the alpha alias is eventually dropped.
  • Policy applies but nothing is blocked. You left validationActions: [Audit] (the library default) or carried over validationFailureAction: Enforce, which ValidatingPolicy does not read. Only validationActions: [Deny] blocks.
  • A privileged init or ephemeral container slips through. Your expression iterated only object.spec.containers. Use the allContainers variable that concatenates all three lists.
  • CEL error on pods with no securityContext. Without .? and orValue, evaluation errors on those pods, and depending on your webhook failurePolicy the request may be allowed through. Keep the optional chaining.
  • Honest scope. This stops the privileged flag, and only that. It does not cover allowPrivilegeEscalation, added Linux capabilities, hostPID or hostNetwork, or host-path mounts. Each of those needs its own validation. A single "block privileged" rule is not a Pod Security Standards restricted profile, the same way a default-deny egress rule limits exfiltration without stopping the initial compromise. Naming what a control leaves open is the difference between a real defense and a checkbox.

Wrap-up

You now have a stable, CEL-based ValidatingPolicy that denies privileged pods across the cluster, handles the null-securityContext case, and covers init and ephemeral containers. It is written against the API that replaced the deprecated ClusterPolicy, not the one every stale example still shows.

Because ClusterPolicy removal is already on Kyverno's roadmap, treat this as the migration template for your other rules: Deny in place of validationFailureAction: Enforce, matchConstraints.resourceRules in place of match.any, and optional chaining everywhere a field can be absent. The natural next policy is disallowing allowPrivilegeEscalation and unlisted capabilities, which lets you stage a full baseline-to-restricted rollout with the same type.

Sources


Originally published at indragustiprasetya.com

Top comments (0)