without guardrails, kubernetes clusters accumulate risk quickly. workloads without resource limits, containers running as root, service accounts with cluster-admin that no one revisits. these are common misconfigurations that admission controllers are designed to prevent.
admission controllers intercept api requests before they hit etcd and decide whether to allow, modify, or reject them. but writing a custom admission webhook means standing up a service, handling tls, dealing with failure modes, and maintaining code that parses admission review objects. that’s a high barrier for enforcing basic requirements like resource limits on all pods.
kyverno lowers that barrier. policies are written in yaml and mirror the structure of the kubernetes resources they act on. no rego, no cel expressions, no new language required.
how it works
kyverno runs as an admission controller inside the cluster. when a resource is created, updated, or deleted, the api server sends it to kyverno, which evaluates it against the configured policies and returns an allow, deny, or mutated version.
there are three policy actions. validate rejects resources that don’t meet the defined standards. mutate modifies resources on the fly to inject defaults. generate creates companion resources automatically when something is deployed.
policies can be cluster-wide with ClusterPolicy or namespace-scoped with Policy. most security baselines use ClusterPolicy to apply them across all namespaces.
validation: the baseline
a common starting point. here’s a policy that rejects any pod running as root:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-run-as-root
spec:
validationFailureAction: Enforce
rules:
- name: check-run-as-non-root
match:
any:
- resources:
kinds:
- Pod
validate:
message: "running as root is not allowed. set securityContext.runAsNonRoot to true."
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
the pattern block mirrors the structure of the resource it’s validating. if the actual resource doesn’t match the pattern, the request is denied with the specified message. no json path expressions, no custom syntax - just the expected shape of the resource.
validationFailureAction has two modes. Enforce blocks non-compliant resources. Audit lets them through but logs a policy violation in the cluster’s event stream and in kyverno’s policy reports. start with Audit in existing clusters to identify non-compliant resources before enforcing.
mutation: injecting defaults
mutation policies allow platform teams to inject labels, resource requests, or security contexts automatically rather than requiring developers to add them manually:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-security-context
spec:
rules:
- name: add-run-as-non-root
match:
any:
- resources:
kinds:
- Pod
mutate:
patchStrategicMerge:
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
every pod that doesn’t already set these fields gets them injected. pods that explicitly set their own values keep them - patchStrategicMerge respects existing fields by default.
this pattern enforces a base security posture without blocking deployments. mutation sets secure defaults, and validation rejects explicit overrides that weaken them.
combining the two results in:
- pods without a security context get a secure one injected
- pods that explicitly set
runAsNonRoot: falseare rejected - the security boilerplate is handled at the policy level rather than per-deployment
generation: companion resources
kyverno can create resources in response to other resources being created. a common use case is network policies - ensuring every new namespace starts with a deny-all ingress rule:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: generate-default-network-policy
spec:
rules:
- name: deny-all-ingress
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
name: default-deny-ingress
namespace: "{{request.object.metadata.name}}"
data:
spec:
podSelector: {}
policyTypes:
- Ingress
every time a namespace is created, kyverno generates a NetworkPolicy inside it that denies all ingress by default. teams then add explicit allow rules for the traffic they actually need.
the synchronize option keeps the generated resource in sync. set it to true on the generate rule and if the network policy is deleted or modified, kyverno recreates it. this ensures baseline policies remain in place regardless of manual changes.
enforcing pod security standards
kubernetes deprecated PodSecurityPolicy in 1.21 and removed it in 1.25. the replacement - pod security admission - provides three profiles at the namespace level with no customization: privileged, baseline, and restricted.
kyverno allows enforcing the same standards with more granular control. the kyverno project maintains a library of policies that map to the pod security standards:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privilege-escalation
spec:
validationFailureAction: Enforce
rules:
- name: check-privilege-escalation
match:
any:
- resources:
kinds:
- Pod
validate:
message: "privilege escalation is not allowed. set allowPrivilegeEscalation to false."
pattern:
spec:
containers:
- securityContext:
allowPrivilegeEscalation: false
=(initContainers):
- securityContext:
allowPrivilegeEscalation: false
=(ephemeralContainers):
- securityContext:
allowPrivilegeEscalation: false
the =() syntax is a conditional anchor - it only applies if the field exists. init containers are optional, so a pod without them should not be rejected on that basis. but if init containers are present, they should follow the same rules.
specific namespaces or service accounts can also be excluded from policies. kube-system workloads often need privileges that application pods should not have:
spec:
rules:
- name: check-privilege-escalation
exclude:
any:
- resources:
namespaces:
- kube-system
- cert-manager
policy reports
kyverno generates PolicyReport and ClusterPolicyReport resources that track compliance across the cluster. these are standard kubernetes resources queryable with kubectl:
kubectl get policyreport -A
each report lists which resources passed, failed, or were skipped for each policy. this is useful for auditing compliance before switching from Audit to Enforce, and for maintaining ongoing visibility into cluster posture.
these reports can be fed into a monitoring stack. the kyverno prometheus metrics endpoint exposes policy violation counts, admission request latency, and error rates. a spike in violations after a deployment indicates a change that the policies flagged.
what to watch out for
kyverno is an admission controller, which means it’s in the critical path of every api request. if kyverno goes down and the webhook’s failure policy is set to Fail, nothing can be deployed. setting failurePolicy to Ignore in production prevents a kyverno outage from freezing the cluster - though policies are not enforced during that window.
kyverno should run with at least three replicas and proper resource limits. the same resource configuration standards applied to application workloads should apply to kyverno itself.
policy ordering matters for mutations. if two policies mutate the same field, the result depends on evaluation order. keeping mutations simple and non-overlapping avoids hard-to-debug ordering issues.
policies can be tested before deployment. kyverno includes a cli for running policies against resources locally:
kyverno apply policy.yaml --resource deployment.yaml
running this in ci catches policy violations before they reach the cluster.
references
[1] kyverno documentation. “introduction.”
kyverno.io/docs/introduction
[2] kyverno documentation. “writing policies.”
kyverno.io/docs/writing-policies
[3] kyverno documentation. “policy reports.”
kyverno.io/docs/policy-reports
[4] kubernetes documentation. “pod security standards.”
kubernetes.io/docs/concepts/security/pod-security-standards
[5] kyverno policies library. “pod security.”
kyverno.io/policies/pod-security
[6] kyverno documentation. “monitoring.”
kyverno.io/docs/monitoring