
Nothing stops kubectl apply from creating a pod that runs as root, pulls :latest, and has no resource limits. Code review can catch it if someone remembers to look. A CI lint step can catch it if the manifest went through CI at all. The only thing that catches it every single time, for every single pod, no matter how it got created, is the cluster itself refusing to schedule it. That's admission control, and Kyverno is the most direct way to get there — policies are plain Kubernetes YAML, not a separate policy language to learn.
This tutorial installs Kyverno, writes three validating policies that block the most common risky patterns, adds a mutating policy that fixes a default instead of just rejecting it, and shows how to roll all of it out without taking down an existing cluster on day one.
Install Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
This installs the admission webhook, the background scanner, and the reporting controller. Give it a minute, then confirm the webhook is live:
kubectl get pods -n kyverno
Policy 1: block containers that can run as root
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-run-as-nonroot
spec:
validationFailureAction: Audit
background: true
rules:
- name: check-runasnonroot
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Running as root is not allowed. Set securityContext.runAsNonRoot: true."
pattern:
spec:
=(securityContext):
=(runAsNonRoot): "true"
containers:
- name: "*"
=(securityContext):
=(runAsNonRoot): "true"
The =() prefix is an optional anchor — it means "if this field is set, it must match," rather than requiring every pod to declare it explicitly in both places. Note validationFailureAction: Audit here, not Enforce — more on why below.
Policy 2: block the latest tag
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Audit
background: true
rules:
- name: require-image-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using the 'latest' tag is not allowed — pin an explicit version."
pattern:
spec:
containers:
- image: "!*:latest"
This also catches an image with no tag at all, since Kubernetes resolves an untagged image to :latest implicitly.
Policy 3: require resource requests and limits
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-requests-limits
spec:
validationFailureAction: Audit
background: true
rules:
- name: validate-resources
match:
any:
- resources:
kinds:
- Pod
validate:
message: "CPU and memory requests and limits are required on every container."
pattern:
spec:
containers:
- resources:
requests:
memory: "?*"
cpu: "?*"
limits:
memory: "?*"
cpu: "?*"
"?*" means "any non-empty value" — the policy doesn't care what the numbers are, only that someone set them deliberately.
Policy 4: mutate instead of reject
Not every gap needs a hard rejection. A missing imagePullPolicy is a good candidate for fixing automatically rather than bouncing the deploy back to the author:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: set-image-pull-policy
spec:
rules:
- name: set-pull-policy
match:
any:
- resources:
kinds:
- Pod
mutate:
patchStrategicMerge:
spec:
containers:
- (image): "*:*"
imagePullPolicy: IfNotPresent
Mutating policies run before validating ones in the admission chain, so a pod can be fixed and then pass validation in the same request.
Audit first, enforce second
Every policy above is set to Audit, not Enforce. In audit mode, Kyverno logs what it would have blocked instead of actually blocking it, and the background scanner evaluates every existing object on a schedule, producing a PolicyReport per namespace:
kubectl get polr -A
Run in audit mode for a few days, review what would have been blocked, fix the genuine violations, and add exceptions for the rest. Only then flip validationFailureAction to Enforce. Going straight to enforce on a live cluster is how a Friday afternoon policy rollout turns into a Friday evening incident.
Exempting namespaces that legitimately need to break the rules
kube-system and most CNI/CSI daemonsets run as root by design — don't try to fix the cluster's own system pods, exempt them explicitly and reviewably with a PolicyException:
apiVersion: kyverno.io/v2
kind: PolicyException
metadata:
name: exempt-kube-system
namespace: kyverno
spec:
exceptions:
- policyName: require-run-as-nonroot
ruleNames:
- check-runasnonroot
- policyName: disallow-latest-tag
ruleNames:
- require-image-tag
match:
any:
- resources:
namespaces:
- kube-system
This is better than editing the policy's match/exclude block directly, because the exception lives as its own object — it shows up in kubectl get polex -A and in a diff review, instead of quietly narrowing the policy itself.
What a block actually looks like
Once a rule is set to Enforce, applying a non-compliant pod fails at admission time, before it's ever scheduled:
$ kubectl apply -f bad-pod.yaml
Error from server: error when creating "bad-pod.yaml": admission webhook
"validate.kyverno.svc-fail" denied the request:
resource Pod/default/bad-pod was blocked due to the following policies
require-run-as-nonroot:
check-runasnonroot: 'validation error: Running as root is not allowed. Set
securityContext.runAsNonRoot: true. rule check-runasnonroot failed at
path /spec/containers/0/securityContext/runAsNonRoot/'
The error names the exact policy, rule, and field path that failed — enough for the person who ran kubectl apply to fix it themselves without filing a ticket.
Common pitfalls
- Enforcing on day one. Audit first. A cluster with months of accumulated "temporary" root pods will have more violations than you expect.
- Forgetting
background: true. Without it, Kyverno only evaluates new admission requests, not objects already running — yourPolicyReportwill look clean while old violations sit untouched. - No exceptions for system namespaces. Without them, enforcing these policies cluster-wide will block legitimate control-plane and CNI components.
- Pattern syntax typos. A missing
=()anchor changes "optional, must match if present" into "required" — test every policy in audit mode against real workloads before enforcing.
Where to go next
Once basic pod hygiene is enforced, Kyverno's verifyImages rule type extends the same admission webhook to check that an image is signed (with Cosign, for example) before it can run at all — turning "block risky pods" into "block risky and unverified pods" with one more rule in the same policy set.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


