Skip to content

All posts

KubernetesKyverno

How to document policy exceptions as audit evidence

Ritesh Patel

Kubernetes: How to document policy exceptions as audit evidence

An exception becomes audit evidence when the cluster can show which policy and resources it covers, that Kyverno applied it, who approved it and when it expires. Kyverno’s PolicyException records the first two and the PolicyReport records the third. Approval and expiry have no field in the Kyverno docs, so store them as annotations beside the manifest.

Checked against the Kyverno v1.19 docs as of 2026-10-27.

What an auditor will ask for

Auditors tend to ask the same five questions about an exception. Each answer needs a home, either in the cluster or in your change record. The table shows where each one lives and whether Kyverno’s own docs define it.

Question Where the answer lives Defined in Kyverno docs?
Which policy is excepted? spec.policyRefs, the policy name and kind Yes
Which resources are covered? spec.matchConditions, a CEL expression Yes
Was the exception applied? A PolicyReport result with result: skip and properties.exceptions Yes
Who approved it? An annotation on the object, plus the ticket or pull request No
When does it expire? An annotation, plus a scheduled review No

The last two rows are gaps in the product, not in your process. Fill them with annotations and an approval record, and say so in the evidence pack rather than implying that Kyverno tracks them.

Turn exceptions on before you write one

PolicyExceptions are off until you enable them. Two container flags control them, and the flags reference lists the defaults.

  1. Set enablePolicyException to true. Its default is false.
  2. Set exceptionNamespace to the namespace where exceptions may live, such as default in the example below.
  3. Leave policyReports on. Its default is true, and it enables the Policy Reports system.

Keeping every exception in one namespace gives the auditor one place to look. The reference is at https://kyverno.io/docs/installation/customization/.

Write the exception as a manifest

Kyverno’s example uses a ValidatingPolicy called require-prod-label and a PolicyException that skips one Deployment by name. The block below keeps that shape and adds three annotations. The annotation keys are our team convention, not Kyverno fields, so Kyverno does not define or check them and the record depends on your process.

apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
  name: exclude-skipped-deployment
  namespace: default
  annotations:
    audit.example.com/approved-by: "security-lead"
    audit.example.com/expires: "2027-01-31"
    audit.example.com/ticket: "SEC-1234"
spec:
  policyRefs:
    - name: require-prod-label
      kind: ValidatingPolicy
  matchConditions:
    - name: skip-by-name
      expression: "object.metadata.name == 'skipped-deployment'"

The docs’ examples use policies.kyverno.io/v1. The docs say it works with CEL-based policy types, including ValidatingPolicy and ImageValidatingPolicy. The spec.policyRefs field names the policy and kind being excluded, and spec.matchConditions narrows the exception to matching resources. In this example, only the Deployment named skipped-deployment in the default namespace is excepted, so keep default in exceptionNamespace.

Show the exception in the policy report

When the match condition is true, Kyverno writes the decision to a PolicyReport. The docs show this result for an exception that fired during a live admission request.

apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
  namespace: default
  labels:
    app.kubernetes.io/managed-by: kyverno
  ownerReferences:
    - apiVersion: apps/v1
      kind: Deployment
      name: skipped-deployment
results:
  - policy: vpol-report-background-sample
    rule: exception
    result: skip
    message: 'rule is skipped due to policy exception: default/exclude-skipped-deployment'
    properties:
      exceptions: exclude-skipped-deployment
      process: admission review
    source: KyvernoValidatingPolicy
    scored: true
scope:
  apiVersion: apps/v1
  kind: Deployment
  name: skipped-deployment
  namespace: default
summary:
  pass: 0
  fail: 0
  warn: 0
  error: 0
  skip: 1

Three fields carry the evidence. result: skip means the rule was not applied. properties.exceptions names the PolicyException that caused it, which is the link back to your approval record. properties.process says whether the decision came from an admission review or a background scan.

Where the record lives and who can read it

The exception is a cluster object, and the proof is a report object. Keep both in places that someone outside the team can reach.

  1. Keep the PolicyException manifest in Git, and make every change through a pull request that names the approver.
  2. List the reports with kubectl get policyreport -A. Run kubectl get polr -o wide for the pass, fail and skip counts, and kubectl get reports -A -o yaml to export the full results.
  3. Give auditors read access. Kyverno’s install creates a ClusterRole named kyverno:rbac:admin:policyreports. The docs show a RoleBinding that grants it to a user or group in one namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: admin-policyreports
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kyverno:rbac:admin:policyreports
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: Group
    name: auditors

Be careful with step 3. The docs say that ClusterRole can perform all operations on policy reports, so binding it to auditors also lets them change reports. The docs I checked don’t provide a read-only role, so write one before you hand out access.

Keep the history the reports drop

Policy reports show the cluster as it is now. The docs say reports “do not record historical information,” and that when a resource is deleted, its entry is removed from the report. Blocked requests don’t appear either. For those, the docs point to the policy rule execution metric and to Kubernetes Events.

Record What it keeps What the docs say it misses
PolicyReport Current results for existing resources Deleted resources, blocked requests, Pod/exec
Policy rule execution metric or Kubernetes Events Blocked admission decisions Retention is not described in the docs I checked
Git history of the PolicyException Every approved change and who made it Only covers exceptions, not results

Export reports on a schedule into dated files, capture the metric or Events in the same export, and keep the ticket ID in the annotation so each row links back to its approval. The auditor then sees both the state and the history.

Audit mode and enforce mode record different things

The effect of an exception depends on the policy’s action. Kyverno’s ClusterPolicy and ValidatingPolicy use different fields for the same idea.

Without an exception ClusterPolicy (failureAction) ValidatingPolicy (validationActions)
Audit: request allowed, violation recorded as fail Audit Audit
Enforce: request blocked, only pass results reported Enforce Deny
With a matching exception: request allowed, skip recorded Both Both

In audit mode, the report is the evidence that the violation existed. In enforce mode, the exception is the evidence that someone chose to let a blocked resource in, so its approval matters more.

If you still run ClusterPolicy

The docs also deprecate spec.validationFailureAction in favour of spec.rules[*].validate[*].failureAction. If your exceptions depend on objects in the kyverno.io API group, check the Kyverno docs for the migration path before you update your evidence pack. Our CEL migration post covers the policy side of that move.

Enforcing this with Kyverno

Kyverno supplies the exception object, the skip result and the report. The approval and expiry process around them is yours to run. A workable routine:

  1. Enable exceptions and set one exceptionNamespace.
  2. Add each exception through a pull request that carries the approver, ticket and expiry annotations.
  3. Export PolicyReports, and the blocked-request metric or Events, on a fixed schedule.
  4. At each review, check every skip entry against an open exception, and remove exceptions past their expiry.

For the wider compliance program, see continuous compliance. For the audit trail that sits next to exceptions, read SOC 2 controls for Kubernetes you can automate. Earlier Nirmata posts cover the exception workflow itself: temporary policy exceptions with Kyverno, streamlining policy exception management and multi-cluster policy exception management.

If you want the product that sits on top of Kyverno, see the Nirmata Control Hub page. This post makes no product claims about it, and the steps above need only Kyverno.

Frequently asked questions

Can a Kyverno PolicyException show who approved it?

Not by itself. The Kyverno docs define no approver field on a PolicyException. Put the approver and the ticket ID in annotations on the object, and keep the approval itself in the ticket or the pull request that added the manifest. An auditor can then match the exception name in a PolicyReport to that approval, and the two records point at each other.

How do I show an auditor that an exception was actually used?

Look for a PolicyReport result with result set to skip and properties.exceptions naming the PolicyException. Kyverno records that result when the exception matches a live admission request.

Does Kyverno remove a PolicyException when it expires?

The PolicyException docs describe no expiry field and no automatic removal. Store the expiry date as an annotation, then run a scheduled review that lists exceptions past their date and deletes or renews them. Keep the review's output with the reports, so the auditor sees the exception was closed on time.

Why are blocked deployments missing from the policy report?

Policy reports describe resources that exist in the cluster. The docs say resources blocked during admission do not appear in a report, and point to the policy rule execution metric or Kubernetes Events for those decisions. Export those signals on the same schedule as the reports, or the blocked requests have no record at all.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Nirmata Control HubOne control plane for Kyverno policies.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.