devsecops· 10 min read

Kyverno: getting started with standard policies

Kyverno’s central idea is that policies should be Kubernetes resources. No new language, no Rego, no separate mental model. A policy is YAML you apply with kubectl, matched against resources with the same selectors you already use.

That design choice is most of why it gets adopted. The learning curve is roughly one afternoon.

Install

helm install kyverno kyverno/kyverno \
  --namespace kyverno --create-namespace \
  --set admissionController.replicas=3

Three replicas is not optional in production. Kyverno sits in the admission path as a webhook. If every replica is down and your webhook failurePolicy is Fail, you cannot admit pods. Run it spread across zones, and give it a PodDisruptionBudget.

Four things a rule can do

Every Kyverno rule does one of four things. Knowing which one you want is most of writing a policy.

Type What it does
validate accept or reject the resource
mutate change the resource before it is stored
generate create other resources when this one appears
verifyImages check image signatures and attestations

generate is the one people miss. It is how you get a default NetworkPolicy, ResourceQuota, and image pull secret into every new namespace without a bootstrap script.

Start in audit, always

The single most important field when you begin:

spec:
  validationFailureAction: Audit   # not Enforce

Audit records violations as PolicyReport resources and admits the resource anyway. Enforce rejects it.

Every policy goes in as Audit first. Leave it a week. Read the reports:

kubectl get policyreport -A
kubectl get clusterpolicyreport

You will find violations in namespaces you forgot existed, and in third-party Helm charts you do not control. Fixing those before flipping to Enforce is the difference between a policy rollout and an incident.

Note that newer Kyverno versions moved this setting under the rule’s validate block as failureAction, with the top-level field deprecated. Check which your version expects. The docs for your installed release are authoritative.

A starter set

These six cover most of the real risk and rarely cause arguments.

Require resource requests. Without requests the scheduler cannot make good decisions, and one unbounded pod can starve a node.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resources
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: check-requests
      match:
        any:
          - resources:
              kinds: [Pod]
      exclude:
        any:
          - resources:
              namespaces: [kube-system, kyverno]
      validate:
        message: "CPU and memory requests are required."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"

"?*" means “any non-empty value”. The containers array pattern applies to every element.

Disallow the latest tag. An image tag that changes under you makes rollbacks meaningless.

    - name: require-image-tag
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Images must use an explicit tag, not :latest."
        pattern:
          spec:
            containers:
              - image: "!*:latest"

Consider requiring digests instead of tags for anything in production. image: "*@sha256:*" is a stricter version of the same idea.

Require probes. A Deployment without a readiness probe will happily receive traffic before it can serve it.

    - name: require-readiness
      match:
        any:
          - resources:
              kinds: [Deployment, StatefulSet]
      validate:
        message: "A readinessProbe is required."
        pattern:
          spec:
            template:
              spec:
                containers:
                  - readinessProbe:
                      "?*": "?*"

Restrict registries. Cheap supply-chain control. Pods may only run images from registries you control.

    - name: allowed-registries
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Images must come from an approved registry."
        pattern:
          spec:
            containers:
              - image: "ghcr.io/your-org/* | *.dkr.ecr.ap-south-1.amazonaws.com/*"

Require ownership labels. Not security, but it decides who gets paged and who pays.

    - name: require-owner
      match:
        any:
          - resources:
              kinds: [Deployment, StatefulSet]
      validate:
        message: "Label 'team' is required."
        pattern:
          metadata:
            labels:
              team: "?*"

Pod Security Standards. Do not hand-write these. Kyverno ships a maintained policy set matching the baseline and restricted profiles:

helm install kyverno-policies kyverno/kyverno-policies \
  --namespace kyverno \
  --set podSecurityStandard=baseline \
  --set validationFailureAction=Audit

Baseline blocks the genuinely dangerous things: host namespaces, privileged containers, hostPath. Restricted additionally requires non-root, dropped capabilities, and seccomp. Most workloads pass baseline unmodified; restricted needs application changes.

Mutation: defaults instead of rejections

The nicest thing about Kyverno is that it can fix a resource instead of rejecting it. Adding a default seccomp profile is friendlier than refusing the pod:

    - name: default-seccomp
      match:
        any:
          - resources:
              kinds: [Pod]
      mutate:
        patchStrategicMerge:
          spec:
            +(securityContext):
              seccompProfile:
                type: RuntimeDefault

The +() anchor means “add this if absent, leave it alone if present”. You get a safer default without overriding teams who made a deliberate choice.

Use mutation for defaults, validation for things that must never happen. A policy that silently changes security-relevant behaviour is worse than one that rejects loudly: mutate to a safer value, never a more permissive one.

Generation: namespace bootstrapping

    - name: default-deny-netpol
      match:
        any:
          - resources:
              kinds: [Namespace]
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny-ingress
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        data:
          spec:
            podSelector: {}
            policyTypes: [Ingress]

synchronize: true means Kyverno restores the NetworkPolicy if someone deletes it. Every new namespace starts closed, without a bootstrap job that someone forgets to run.

Rolling out without making enemies

  1. Install, then install kyverno-policies at baseline, everything in Audit.
  2. Wait a week. Read the PolicyReports.
  3. Publish what you found. Per team, with counts. Most violations are one bad Helm chart default repeated fifty times.
  4. Fix the platform-owned violations yourself. Do not ask teams to fix things you shipped.
  5. Flip policies to Enforce one at a time, easiest first. Requests and labels before Pod Security.
  6. Exclude namespaces explicitly and visibly rather than weakening a policy for everyone.

The failure mode is going straight to Enforce cluster-wide because the policies look obviously correct. They are correct. They will also block a deploy for a team that has never heard of Kyverno, at which point the policy engine becomes the thing that broke production, and you will spend more effort rebuilding trust than you saved.

Where this is heading

Kubernetes now has built-in ValidatingAdmissionPolicy using CEL, which handles simple validation without a webhook in the path. It is genuinely better for the “reject if field is missing” case: no webhook availability to worry about.

It does not do mutation, generation, or image verification. Kyverno can also generate ValidatingAdmissionPolicy resources from its own policies, which is a reasonable direction: author once, let the API server enforce the simple cases natively and Kyverno handle the rest.