Why Kyverno
Kyverno was designed specifically for Kubernetes. Unlike OPA/Gatekeeper which requires learning the Rego language, Kyverno policies are pure YAML — the same format you already use for every other Kubernetes resource.
- No new language — Policies are standard Kubernetes resources written in YAML
- Built for K8s — Understands Kubernetes resource structure natively
- Multiple rule types — validate, mutate, generate, and verifyImages in one engine
- Policy Reports — Generates PolicyReport/ClusterPolicyReport CRDs for audit visibility
- kubectl apply — Deploy policies the same way you deploy any other K8s manifest
Install Kyverno
Kyverno is installed via Helm. The chart deploys the admission webhook controller, background controller (for generate rules), and reports controller.
# Add the Kyverno Helm repository
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
# Install Kyverno in its own namespace
helm install kyverno kyverno/kyverno \
--namespace kyverno \
--create-namespace \
--set replicaCount=3
# Verify pods are running
kubectl get pods -n kyverno
# NAME READY STATUS RESTARTS AGE
# kyverno-admission-controller-xxxxx 1/1 Running 0 30s
# kyverno-background-controller-xxxxx 1/1 Running 0 30s
# kyverno-cleanup-controller-xxxxx 1/1 Running 0 30s
# kyverno-reports-controller-xxxxx 1/1 Running 0 30s
# Verify the webhook is registered
kubectl get validatingwebhookconfigurations | grep kyverno
# kyverno-resource-validating-webhook-cfg
kubectl get mutatingwebhookconfigurations | grep kyverno
# kyverno-resource-mutating-webhook-cfg
replicaCount=3 for high availability. The admission controller is in the critical path of all API server requests — if it's down, pods can't be created (unless you configure failurePolicy: Ignore).
ClusterPolicy — Validate Rules
Validate rules check resources against conditions and reject non-compliant requests. The validationFailureAction field controls behavior: Enforce blocks the request; Audit logs a violation but allows the request.
Require Labels
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
annotations:
policies.kyverno.io/title: Require Labels
policies.kyverno.io/category: Best Practices
policies.kyverno.io/severity: medium
policies.kyverno.io/description: >-
Require that all Pods have 'app.kubernetes.io/name'
and 'app.kubernetes.io/managed-by' labels.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-labels
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
The labels 'app.kubernetes.io/name' and
'app.kubernetes.io/managed-by' are required.
pattern:
metadata:
labels:
app.kubernetes.io/name: "?*"
app.kubernetes.io/managed-by: "?*"
The "?*" pattern means "any non-empty string." If a Pod is created without these labels, Kyverno rejects the request with the specified message.
Disallow Latest Tag
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
annotations:
policies.kyverno.io/title: Disallow Latest Tag
policies.kyverno.io/category: Best Practices
policies.kyverno.io/severity: medium
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-image-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must use a specific tag, not ':latest' or no tag."
pattern:
spec:
containers:
- image: "!*:latest & *:*"
- name: validate-init-container-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Init containers must use a specific tag."
pattern:
spec:
=(initContainers):
- image: "!*:latest & *:*"
The pattern "!*:latest & *:*" means: NOT ending in :latest AND must contain a colon (i.e., must have an explicit tag). The =(initContainers) syntax is an "anchor" — it means "if initContainers exists, then validate it" (optional field check).
Require Resource Limits
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
annotations:
policies.kyverno.io/title: Require Resource Limits
policies.kyverno.io/category: Best Practices
policies.kyverno.io/severity: high
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-resources
match:
any:
- resources:
kinds:
- Pod
validate:
message: >-
All containers must have CPU and memory requests and limits defined.
pattern:
spec:
containers:
- resources:
requests:
memory: "?*"
cpu: "?*"
limits:
memory: "?*"
cpu: "?*"
Test the policy:
# Apply the policy
kubectl apply -f require-resource-limits.yaml
# This pod will be REJECTED — no resource limits
kubectl run test-no-limits --image=nginx:1.25
# Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
# resource Pod/default/test-no-limits was blocked due to the following policies:
# require-resource-limits:
# validate-resources: "All containers must have CPU and memory requests and limits defined."
# This pod will be ACCEPTED
kubectl run test-with-limits --image=nginx:1.25 \
--requests='cpu=100m,memory=128Mi' \
--limits='cpu=200m,memory=256Mi'
# pod/test-with-limits created
Policy vs ClusterPolicy
Kyverno provides two scopes for policies:
- ClusterPolicy — Applies to resources across all namespaces. Use for organization-wide standards.
- Policy — Namespace-scoped. Applies only to resources in the same namespace as the policy. Use for team-specific rules.
# Namespace-scoped policy — only affects resources in 'production' namespace
apiVersion: kyverno.io/v1
kind: Policy
metadata:
name: production-require-probes
namespace: production
spec:
validationFailureAction: Enforce
rules:
- name: require-readiness-probe
match:
any:
- resources:
kinds:
- Deployment
validate:
message: "Production Deployments must have readinessProbe defined."
pattern:
spec:
template:
spec:
containers:
- readinessProbe:
httpGet:
path: "?*"
port: "?*"
Match & Exclude
Every rule must define which resources it applies to via match. You can optionally exclude specific resources, namespaces, or label selectors.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-nodeport
spec:
validationFailureAction: Enforce
rules:
- name: deny-nodeport
match:
any:
- resources:
kinds:
- Service
# Only match in these namespaces
namespaces:
- production
- staging
exclude:
any:
- resources:
# Exclude services with this label
selector:
matchLabels:
allow-nodeport: "true"
- subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: ci-system
validate:
message: "NodePort services are not allowed. Use LoadBalancer or ClusterIP."
pattern:
spec:
type: "!NodePort"
match.any means "if ANY of these conditions match, apply the rule." match.all means "ALL conditions must match." Same logic for exclude. You can combine resource kinds, namespaces, labels, annotations, and subjects (users/service accounts).
Exercises
production namespace. Use a validate rule with the pattern spec.replicas and the operator ">=2". Test with kubectl create deployment test --image=nginx --replicas=1 -n production.
securityContext.runAsNonRoot must be true). Exclude the kube-system namespace. Verify by deploying a pod without runAsNonRoot: true.
ghcr.io/myorg/* and docker.io/library/*. Use the validate.deny form with conditions and the !~ (not match) operator on container images.
Key Takeaways & Next Steps
- Kyverno policies are standard Kubernetes CRDs —
kubectl applyto deploy validationFailureAction: Enforceblocks;Auditlogs only- Pattern matching uses anchors:
"?*"(non-empty),"!value"(not equal),=(field)(if exists) - ClusterPolicy = cluster-wide; Policy = namespace-scoped
- Match/Exclude support kinds, namespaces, labels, annotations, and subjects
Next in the Series
In Part 2: Mutate & Generate, we'll write mutate rules to auto-inject labels and sidecars, and generate rules to auto-create NetworkPolicies when namespaces are created.