kontrolplane
not accepting clients |
english english nederlands nederlands
← back to blog

policy as code with kyverno

kyverno enforces security standards, injects defaults, and generates resources in kubernetes using plain yaml. policies are written in the same structure as the resources they act on - no new language required.

share

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:

  1. pods without a security context get a secure one injected
  2. pods that explicitly set runAsNonRoot: false are rejected
  3. 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

[ ready when you are ]

no forms.
no calls.
just email.