not accepting clients
← back to blog

policy as code with kyverno

kyverno enforces security standards, injects defaults, and generates resources in kubernetes using plain yaml policies instead of a new language.

kubernetes clusters without guardrails pick up the same misconfigurations quickly: workloads without resource limits, containers running as root, service accounts with cluster-admin that no one revisits. admission controllers are designed to prevent exactly these.

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, so writing one does not mean learning rego, cel, or any other new language.

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 is 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, with no json path expressions or custom syntax. if the actual resource doesn’t match the pattern, the request is denied with the specified message.

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 let platform teams inject labels, resource requests, or security contexts automatically, so developers don’t have to add them by hand:

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, because of the +() add-if-not-present anchor: without it, patchStrategicMerge overwrites whatever the pod set.

this pattern enforces a base security posture without blocking deployments. mutation injects a secure security context into pods that don’t have one, validation rejects explicit overrides that weaken it (a pod that sets runAsNonRoot: false), and the security boilerplate lives in the policy instead of in every deployment.

generation: companion resources

kyverno can create resources in response to other resources being created. a common use case is giving every new namespace a network policy 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 need.

the synchronize option keeps the generated resource in sync. set it to true on the generate rule and kyverno recreates the network policy if it is deleted or modified, so manual changes cannot remove the baseline.

enforcing pod security standards

kubernetes deprecated PodSecurityPolicy in 1.21 and removed it in 1.25. its 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

# ask the author

a question
about this
post?

direct line