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
ValidatingPolicyCEL engine builds on the upstream ValidatingAdmissionPolicy machinery that stabilized in 1.30. -
Kyverno 1.18+ installed. Older releases only expose
ValidatingPolicyunderpolicies.kyverno.io/v1alpha1; thev1version arrives in 1.18. -
kubectlwith 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
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"}'
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'
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."
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
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
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
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.
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 readserror: 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
v1alpha1from the policy library. Kyverno's own publishedpod-security-vpolpages still carryapiVersion: policies.kyverno.io/v1alpha1andminversion: 1.14.0. On a 1.18 cluster preferv1. Thev1alpha1alias 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 overvalidationFailureAction: Enforce, whichValidatingPolicydoes not read. OnlyvalidationActions: [Deny]blocks. -
A privileged init or ephemeral container slips through. Your expression iterated only
object.spec.containers. Use theallContainersvariable that concatenates all three lists. -
CEL error on pods with no securityContext. Without
.?andorValue, evaluation errors on those pods, and depending on your webhookfailurePolicythe request may be allowed through. Keep the optional chaining. -
Honest scope. This stops the
privilegedflag, and only that. It does not coverallowPrivilegeEscalation, added Linux capabilities,hostPIDorhostNetwork, 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
- Kyverno docs, ValidatingPolicy
- CNCF, Announcing Kyverno release 1.18
- Kyverno policy library, Disallow Privileged Containers in ValidatingPolicy
- Kyverno blog, Announcing Kyverno Release 1.17
- Kyverno GitHub, Issue #12441: default Helm CRD values don't create ValidatingPolicy
Originally published at indragustiprasetya.com
Top comments (0)