
Governance
Kyverno vs OPA Gatekeeper: Policy as Code at Cluster Scale
Kyverno against OPA Gatekeeper on Kubernetes, then the lifecycle nobody budgets for: versioning, testing, exceptions and reporting are 80% of the work.
Every Kubernetes platform has a policy problem. In the early days it looks manageable: a handful of webhook configurations, a naming convention document, a Slack reminder before production deploys. Then the engineer who built the admission control leaves. The document goes stale. A pod without resource limits triggers a node-level overcommit cascade that the scheduler cannot resolve, and the post-mortem reveals that the control everyone assumed was enforced lived only in one person's memory.
This is the policy debt trap. It is not a Kubernetes problem and it is not a tooling problem. It is an organizational problem that tooling can address only if you treat policies the way you treat code: version-controlled, CI-tested, reviewed, pipeline-deployed, and monitored for regression. This article skips the Kyverno-versus-OPA conference circuit. The 2026 question is how to build a policy corpus that developers can reason about, that platform teams can extend without breaking production, and that auditors can trust without a manual evidence hunt. The engine is 20% of that problem. The lifecycle is 80%.
What follows is the architecture of a Kyverno deployment built for the long game: one that a regulated platform team can still operate after the original authors leave, that generates continuous compliance evidence without a quarterly scramble, and that treats every control as a tested, versioned, enforced artifact. Kyverno 1.17 made green-field CEL policy types stable on policies.kyverno.io/v1 and tightened alignment with native ValidatingAdmissionPolicies — but the lifecycle patterns matter more than any single release.
The Policy Debt Trap: Why Most Kubernetes Security Controls Are One Engineer's Retirement Away from Gone
The failure mode is consistent across organizations of every size. A motivated senior engineer implements a set of admission controls — hand-registered ValidatingWebhookConfigurations or early Gatekeeper ConstraintTemplates. The controls work. That engineer knows why each rule exists, which informal exceptions were granted, and which incident or control each maps to. Then they leave.
What remains is a set of webhook registrations the new team is afraid to modify, ConstraintTemplates with no test suite, and a README last updated in 2023. The worst outcome is not that controls break — it is that the team cannot tell whether they are working. Admission webhooks fail closed by default — failurePolicy defaults to Fail — so the inherited risk is a webhook someone explicitly set to failurePolicy: Ignore, which silently admits everything when the webhook is unreachable. Background scans surface violations in PolicyReport CRDs that no one reads. The controls persist as configuration archaeology: present, inherited, and no longer understood.
The solution is not better documentation. It is making the policies themselves the source of truth, in a form that a CI system can test and a GitOps pipeline can deploy. A YAML policy with a kyverno test suite that runs on every pull request is durable in a way a Slack thread never is. Institutional memory lives in the tests, the PR history, and the metadata that links each rule to the compliance control it satisfies.
Policy as Code vs. Policy as Configuration: Why the Distinction Matters
The term 'policy as code' has been diluted. At one end of the spectrum it means nothing more than storing YAML files in a Git repository. At the other end it means applying the full software development lifecycle to policies: branching, review, automated tests, staged rollout, versioned releases, and deprecation. These are not the same thing — and the gap between them is where most Kyverno deployments fail.
Policy as configuration treats the YAML as an artifact to be deployed, like a Helm chart value or a ConfigMap. You write it, you apply it, you move on. Policy as code treats the YAML as source that must be validated against a suite of known-good and known-bad fixtures before it is trusted in any cluster. Without that suite, a validate rule change can regress a team's deploy path until it blocks them in production — or worse, silently stop matching the resources it was meant to protect.
The practical implication is that every policy in the corpus — green-field ValidatingPolicy / MutatingPolicy YAML or any remaining legacy ClusterPolicy — needs a kyverno test suite in the same directory, committed together, gated by CI. The suite is the contract: what the policy allows and denies is explicit, and a failing case cannot merge. This is the same discipline applied to application code — it is just rarely applied to infrastructure policies with the same rigor.
Kyverno's Architecture: Admission, Background Scan, and the Generate/Mutate Pipeline
Before building the lifecycle, be precise about what Kyverno does. The architecture diagram below shows the full control loop: two distinct paths — the synchronous admission webhook and the asynchronous background controller — that every policy in the corpus must be designed against.
The admission webhook intercepts CREATE, UPDATE, and DELETE operations against the API server and evaluates policies synchronously. validate allows or denies; mutate patches the object before it is persisted (resource limits, securityContext defaults, required labels); generate creates dependent resources when a trigger fires — commonly NetworkPolicy or ResourceQuota on namespace create. On mixed-runtime clusters, reject Pods whose runtimeClassName is unprovisioned and require a matching nodeSelector where a second RuntimeClass such as Wasm is in play.
The background controller runs independently of the admission path. It periodically lists existing resources against policies that opt into background evaluation — spec.background: true on legacy ClusterPolicy, or spec.evaluation.background.enabled on ValidatingPolicy (default true when unset) — and writes PolicyReport / ClusterPolicyReport CRDs. Resources that predate a policy are not remediated at admission time — background scan is the only way to surface pre-existing violations, and it is where most teams discover their 'enforced' controls only covered new objects.
A regulated platform's resource-limits policy looks like the following green-field ValidatingPolicy. It requires every container to declare requests and limits — the usual root of node-level overcommit incidents — using CEL under spec.validations and matchConstraints for selection, with system namespaces excluded via matchConditions.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-resource-limits
annotations:
policies.kyverno.io/title: "Require Resource Limits"
policies.kyverno.io/category: "Resource Management"
policies.kyverno.io/severity: "high"
policies.kyverno.io/subject: "Pod"
policies.kyverno.io/minversion: "1.17.0"
policies.kyverno.io/description: >
Require CPU and memory requests and limits on all containers.
compliance.stribog.io/control: "NIS2-RES-04, CIS-K8s-5.2.4"
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
matchConditions:
- name: exclude-system-namespaces
expression: "!(request.namespace in ['kube-system', 'kyverno'])"
validations:
- expression: >
object.spec.containers.all(c,
has(c.resources) &&
has(c.resources.requests) &&
has(c.resources.limits) &&
has(c.resources.requests.cpu) &&
has(c.resources.requests.memory) &&
has(c.resources.limits.cpu) &&
has(c.resources.limits.memory)
)
message: >
Every container must declare resources.requests.cpu,
resources.requests.memory, resources.limits.cpu, and
resources.limits.memory.Notice the annotation block. The compliance.stribog.io/control annotation links this policy to specific control identifiers in your framework. Tools like KubeVigil can cross-reference PolicyReport CRDs against those annotations and produce per-control evidence packs for auditors — turning the policy corpus into a living compliance artifact rather than a static document that drifts from production reality.
Kyverno vs OPA Gatekeeper: What Actually Separates Them in 2026
Both are CNCF projects, both are admission webhooks, both do background scanning. They surface results differently: Kyverno writes PolicyReport CRDs — an open Kubernetes Policy WG format rather than a Kyverno invention — while Gatekeeper records violations in the status of each Constraint, which an adapter has to translate before a shared dashboard can read both. Two of the differences people still cite have expired. Gatekeeper has had mutation as a stable feature since v3.10 (Assign, AssignMetadata, ModifySet, AssignImage), so "Kyverno can mutate and Gatekeeper cannot" is four years out of date. And Kyverno's move to CEL means the "YAML versus Rego" framing no longer separates them either — on the CEL surface both projects express constraints in the same language Kubernetes itself uses for ValidatingAdmissionPolicy.
What is left is narrower and more useful. Gatekeeper has no equivalent of Kyverno's generate rule — policy-driven creation of dependent resources, so that a new namespace arrives with its NetworkPolicy, ResourceQuota and image-pull secret already in place. That is the capability that turns an admission controller into a provisioning mechanism, and if you need it, the choice is made. Running against it: Rego is a real language and Gatekeeper's constraint-template model is genuinely more expressive for policies whose logic does not decompose into a match-and-assert shape. A team that already has Rego expertise from OPA elsewhere in the stack is paying a tax to move.
Kyverno, CEL, and Native Kubernetes Admission APIs: The Convergence
Kyverno 1.17 promoted CEL-based policy types (ValidatingPolicy, MutatingPolicy, GeneratingPolicy, and related kinds) to policies.kyverno.io/v1 GA — the stable green-field surface — and deprecated legacy ClusterPolicy. Kubernetes 1.30 also stabilized native ValidatingAdmissionPolicy, a CEL admission path without a third-party webhook. Kyverno still evaluates legacy ClusterPolicy rules; on the new surface, a ValidatingPolicy can optionally generate a native ValidatingAdmissionPolicy via spec.autogen.validatingAdmissionPolicy.enabled.
When VAP generation is fully enabled, those CEL validations can run in-process in the API server — no network hop to Kyverno pods — which matters for high-frequency resources such as Pods. A bare ValidatingPolicy without per-policy autogen (and a bare legacy ClusterPolicy validate.cel rule without validate.cel.generate) stays on the Kyverno webhook path until the global generator and the per-policy or per-rule opt-in are both in place.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-non-root
annotations:
policies.kyverno.io/title: "Require Non-Root Containers (CEL)"
policies.kyverno.io/category: "Pod Security"
policies.kyverno.io/severity: "critical"
policies.kyverno.io/minversion: "1.17.0"
compliance.stribog.io/control: "NIS2-SEC-11, CIS-K8s-5.2.6"
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
validations:
- expression: >
object.spec.containers.all(c,
has(c.securityContext) &&
has(c.securityContext.runAsNonRoot) &&
c.securityContext.runAsNonRoot == true
) &&
(!has(object.spec.initContainers) ||
object.spec.initContainers.all(c,
has(c.securityContext) &&
has(c.securityContext.runAsNonRoot) &&
c.securityContext.runAsNonRoot == true
)
) &&
(!has(object.spec.ephemeralContainers) ||
object.spec.ephemeralContainers.all(c,
has(c.securityContext) &&
has(c.securityContext.runAsNonRoot) &&
c.securityContext.runAsNonRoot == true
)
)
message: >-
All containers, initContainers, and ephemeralContainers must
set securityContext.runAsNonRoot=true.
Running as root is prohibited in production namespaces.As written, this ValidatingPolicy is evaluated by the Kyverno admission webhook. The claimed in-process ValidatingAdmissionPolicy path on the pinned 1.17.2 line is three layers deep. Leave the admission-controller toggle --generateValidatingAdmissionPolicy enabled — it defaults to true on the 1.17.2 binary and Helm chart (features.generateValidatingAdmissionPolicy.enabled); the flag description is 'set to false to disable,' and the admission-policy generator gates both green-field ValidatingPolicy and legacy ClusterPolicy on that same toggle. Then opt each policy in: for ValidatingPolicy set spec.autogen.validatingAdmissionPolicy.enabled: true (default false when unset); for legacy ClusterPolicy CEL set validate.cel.generate: true and keep the rule inside shapes the generator accepts (typically a single CEL validate rule with match/exclude shapes that translate to native VAP). Ensure the admission controller ServiceAccount can create and update ValidatingAdmissionPolicies and Bindings. PolicyReport coverage for those generated VAPs is a separate reports-controller switch — keep --validatingAdmissionPolicyReports=true (also default true on 1.17.2 binary/Helm). Disable the global generator, skip the per-policy or per-rule opt-in, or lack VAP RBAC, and you stay on webhook-path latency.
CEL is the same language Kubernetes uses internally for validating native API objects. Engineers who write CEL for custom resource validation can apply the same skill to Kyverno admission policies — a thinner abstraction over the platform rather than a competing policy language, and one that transfers cleanly if a simple rule later graduates into a native ValidatingAdmissionPolicy.
Building the Policy Corpus: Taxonomy, Naming, and Ownership at Scale
A policy corpus without a taxonomy is a collection of files. A taxonomy without ownership is a governance document no one maintains. Encode both in the repository layout and in the policy metadata — not in a separate spreadsheet that will rot.
The taxonomy that works at scale organizes policies by category and severity. Category maps to the compliance domain: pod-security, resource-management, network, rbac, image-integrity, namespace-hygiene. Severity is business impact: critical (block immediately with Deny / Enforce), high (enforce after a migration window), medium (audit and dashboard), low (informational). Every policy gets both so an auditor can ask: show every critical control and prove it is enforced on every cluster.
Naming convention follows {category}-{control-name} in kebab-case: pod-security-require-non-root, resource-management-require-limits. Consistent names keep GitOps queries, dashboards, and kyverno test invocations greppable across a multi-year corpus.
- Repository structure: one directory per policy category; each policy has its own subdirectory containing the policy YAML and a
kyverno-test.yamltest suite. - Ownership annotation:
policies.kyverno.io/owner: team-platform— the team accountable for correctness and the first contact for exception requests. - Compliance mapping annotation:
compliance.stribog.io/control: NIS2-RES-04— links to a control registry that maps control IDs to audit evidence requirements. - Minimum version annotation:
policies.kyverno.io/minversion— documents the oldest Kyverno version the policy is known to work on (policy-library / catalog metadata). Kyverno does not enforce it at admit or reconcile time; if you need a real version gate, compare the annotation to the target cluster'skyverno versionoutput in CI before sync. - Mode annotation:
policies.kyverno.io/mode: Deny|Audit(orEnforce|Auditon legacy rules) — the intended production mode, distinct from the currently deployedvalidationActions/validate.failureActionduring rollout.
Mutate fails differently from validate. A failing validate blocks with a visible message; a failing mutate silently admits without the patch. Always pair mutate with a validate that asserts the expected fields so the failure mode is visible. The co-design pattern below is shown as legacy ClusterPolicy (migration-era surface still common in existing corpora); for green-field work, express the same pair as MutatingPolicy + ValidatingPolicy under policies.kyverno.io/v1.
# Policy 1: Mutate — inject default security context
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: pod-security-inject-security-context
annotations:
policies.kyverno.io/title: "Inject Default Security Context"
policies.kyverno.io/category: "pod-security"
policies.kyverno.io/severity: "high"
spec:
rules:
- name: add-security-context
match:
any:
- resources:
kinds: [Pod]
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
securityContext:
+(allowPrivilegeEscalation): false
+(readOnlyRootFilesystem): true
+(runAsNonRoot): true
---
# Policy 2: Validate — confirm injection was applied or explicitly overridden
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: pod-security-require-security-context
annotations:
policies.kyverno.io/title: "Require Security Context Fields"
policies.kyverno.io/category: "pod-security"
policies.kyverno.io/severity: "critical"
spec:
background: true
rules:
- name: check-security-context
match:
any:
- resources:
kinds: [Pod]
validate:
failureAction: Enforce
message: "All containers must have allowPrivilegeEscalation=false, readOnlyRootFilesystem=true, and runAsNonRoot=true"
pattern:
spec:
containers:
- (name): "*"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: trueTesting Policies Like Code: Kyverno CLI, Chainsaw, and PR Gates
kyverno test is the core unit gate: it evaluates a policy against resource fixtures and expected results (allow, deny, mutate). Covering pass and fail paths for every rule — or every ValidatingPolicy validation set — is the contract between the author and every engineer subject to that policy. For CEL policy types, each result row needs isValidatingPolicy: true so the CLI selects the ValidatingPolicy engine path rather than the legacy ClusterPolicy evaluator.
Each policy directory holds a kyverno-test.yaml that references fixtures and expected results. CI enforces that layout contract first: every policies/<category>/<policy>/ directory must contain a same-directory kyverno-test.yaml (a missing suite fails the job even when other policies still have tests). Then kyverno test policies/ --require-tests walks the tree, fails if zero suites are found anywhere, and reports pass/fail (with mutation diffs). Together those steps are the PR gate on every policy change — including refactors that migrate a legacy ClusterPolicy onto policies.kyverno.io/v1.
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
name: require-resource-limits
policies:
- require-resource-limits.yaml
resources:
- good-pod.yaml
- bad-pod-no-limits.yaml
- bad-pod-no-requests.yaml
results:
- policy: require-resource-limits
isValidatingPolicy: true
resources:
- good-pod
kind: Pod
result: pass
- policy: require-resource-limits
isValidatingPolicy: true
resources:
- bad-pod-no-limits
kind: Pod
result: fail
- policy: require-resource-limits
isValidatingPolicy: true
resources:
- bad-pod-no-requests
kind: Pod
result: failFor more complex integration scenarios — particularly for generate policies that create dependent resources, or for policies that depend on live cluster state — Chainsaw runs declarative end-to-end sequences against a real cluster (often kind in CI). It is slower than kyverno test but catches races in background scans, generate/namespace selector interactions, and exception CRD precedence that unit fixtures cannot simulate.
#!/usr/bin/env bash
# .github/workflows/policy-ci.yaml (excerpt — bash step)
set -euo pipefail
# Install kyverno CLI (pinned to cluster version)
KYVERNO_VERSION="v1.17.2"
curl -fsSL "https://github.com/kyverno/kyverno/releases/download/${KYVERNO_VERSION}/kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz" \
| tar xz kyverno
chmod +x kyverno && mv kyverno /usr/local/bin/kyverno
# Layout contract: policies/<category>/<policy>/ must ship kyverno-test.yaml
# --require-tests alone only fails when zero suites exist repo-wide
missing=0
while IFS= read -r -d '' dir; do
if [[ ! -f "${dir}/kyverno-test.yaml" ]]; then
echo "missing kyverno-test.yaml in ${dir}" >&2
missing=1
fi
done < <(find policies -mindepth 2 -maxdepth 2 -type d -print0)
[[ "${missing}" -eq 0 ]] || exit 1
# Execute suites; --require-tests fails if none found at all
kyverno test policies/ --require-tests -v 4
echo "All policy tests passed."The diagram places CI in the full policy lifecycle and shows how exception requests keep an audit trail. PolicyException objects travel the same GitOps path as the policies they carve out — reviewed, tested where the CLI supports exceptions: fixtures, and merged with an expiry annotation the platform team can grep.
Exception Management Without Loopholes: Time-Bound Exceptions with Audit Trails
Policy exceptions are not optional in a real organization. A team migrating a legacy workload will have a transitional period where the workload cannot yet meet a new runAsNonRoot requirement; a vendor image may not support a read-only root filesystem for another release cycle. Pretending exceptions do not exist pushes teams to bypass the enforcement layer entirely — worse than a structured exemption because it produces no audit trail and no owner for remediation.
Kyverno's PolicyException CRD is the right abstraction: a namespaced object that exempts matched resources from a named policy for a defined period, with a stated justification. Submit it as a pull request, review with the policy-owning team, and merge like any other change. The Git history is the audit trail — not a Slack thread, not a one-off kubectl annotate.
There are two PolicyException APIs. For green-field policies.kyverno.io/v1 types (ValidatingPolicy, MutatingPolicy, GeneratingPolicy, ImageValidatingPolicy, …), use apiVersion: policies.kyverno.io/v1 kind: PolicyException with spec.policyRefs (name + kind) and CEL spec.matchConditions. For legacy ClusterPolicy/Policy only, the older apiVersion: kyverno.io/v2 form with spec.exceptions[].policyName / ruleNames and spec.match still applies — it does not exempt CEL policy types. Mixing them is a silent non-exemption.
PolicyExceptions are off by default on both surfaces. The admission controller must run with --enablePolicyException=true and --exceptionNamespace=<ns> (or * to allow every namespace). The object must live in a namespace the flag permits — this example uses metadata.namespace: payments, so set --exceptionNamespace=payments (or *), or place the object in your configured exception namespace. The PE namespace need not match the workload namespace: a platform-owned policy-exceptions namespace is a common pattern when RBAC should limit who can mint carve-outs. Applying the CRD alone does not grant an exemption if the feature is disabled — that is the same silent no-op whether the object is policies.kyverno.io or kyverno.io/v2.
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
name: vendor-legacy-app-exception
namespace: payments
annotations:
policies.kyverno.io/minversion: "1.17.0"
exception.stribog.io/requestor: "team-payments"
exception.stribog.io/approver: "team-platform"
exception.stribog.io/ticket: "JIRA-4821"
exception.stribog.io/justification: >
vendor/legacy-payment-processor:3.1.4 image runs as uid 0 by design.
Vendor has committed to a non-root build in the next release (Q3 2026).
Exception expires 2026-09-01. Team is pinned to this image version only.
exception.stribog.io/expires: "2026-09-01"
spec:
policyRefs:
- name: require-non-root
kind: ValidatingPolicy
matchConditions:
- name: match-legacy-payment-processor
expression: >
object.metadata.namespace == 'payments' &&
has(object.metadata.labels) &&
has(object.metadata.labels['app.kubernetes.io/name']) &&
object.metadata.labels['app.kubernetes.io/name']
== 'legacy-payment-processor'Expiry is process-enforced, not API-enforced. A nightly job should list PolicyException objects (both API groups if you still run legacy policies), compare exception.stribog.io/expires to today, and open a ticket for anything within 30 days of expiry. Remove expired exceptions via PR — if the workload is still non-compliant, that PR is the forcing function at the right organizational level, not an emergency page at admission time.
A structured exception with an audit trail is not a loophole. An undocumented `--dry-run=server` flag added to a deploy pipeline to bypass a blocking policy is a loophole. The goal of exception management is to make the structured path easier than the bypass path.
Wiring Kyverno Reports to Your Compliance Dashboard
PolicyReport CRDs are the native output of Kyverno's background controller. A namespaced PolicyReport holds one result per resource per rule (reference, policy, rule, pass/fail/warn/skip/error, message); ClusterPolicyReport covers cluster-scoped resources. That machine-readable evidence is what tools like KubeVigil consume on out-of-band audit passes.
The compliance dashboard architecture for a multi-cluster deployment has three layers. The first layer is per-cluster: Kyverno continuously writing PolicyReports. The second is aggregation: a collector (Prometheus or Policy Reporter) scrapes or watches those CRDs across clusters into a central metrics store. The third is presentation: Grafana or a custom UI that shows posture per cluster, per control, and per category.
The critical design decision is the independence of the audit layer. Kyverno's PolicyReport CRDs report what Kyverno has evaluated — they do not report coverage gaps, policies left in Audit that should be Deny/Enforce, or accidental namespace exclusions. KubeVigil re-evaluates workloads against its own 150-check suite without trusting Kyverno's reports. Continuous in-cluster reports plus scheduled out-of-band scans give defense in depth on the evidence chain.
For regulated fintech workloads specifically, the audit evidence requirement goes beyond a dashboard screenshot. Auditors need to answer: for this control, on this resource, at this time, was the policy enforced? PolicyReports with timestamps and resource UIDs, stored in append-only object storage, plus the Git commit of the policy in effect, form a provenance chain — the same shape SOC 2 CC6/CC8 ask for on access control and change management.
Building the Governance Layer for Decades: Organizational Durability Over Tool Optimization
The Kyverno project will change. CEL is tightening the alignment with native Kubernetes admission APIs, and it is plausible that some simple validate cases move fully into the platform. What is not a commodity — and will not become one — is your tested, ownership-tagged corpus, the CI that gates every change to it, and years of exception audit trail in Git.
This is what 'the long game' means in practice for governance. An organization that treats its policy corpus as living software — with ownership, tests, and deprecation processes — can migrate between admission engines far more cheaply than one sitting on 200 hand-applied ConstraintTemplates and a tribal knowledge base. Invest in lifecycle discipline, not the tool brand.
The capabilities page describes how Stribog builds these governance layers for regulated platforms. The engagement starts with an audit of what is enforced, what is assumed to be enforced, and what is missing, then produces a prioritized remediation roadmap. The output is not a Kyverno installation — it is a policy corpus with tests, ownership, a delivery pipeline, and an exception process that still runs after the engagement ends.
Policy as code, executed rigorously, is one of the highest-leverage infrastructure investments a regulated platform team can make: automatic admission enforcement, continuous compliance evidence, time-boxed exceptions with a Git audit trail, and a tested versioned corpus — a governance layer auditors trust, developers understand, and no single engineer owns alone.
§FAQ/Common questions
Frequently asked
Should I use Kyverno or OPA Gatekeeper in 2026?
The debate is less important than the lifecycle discipline you wrap around either engine. That said, Kyverno's CEL support and alignment with native Kubernetes ValidatingAdmissionPolicies gives it a structural advantage for new deployments: you get a unified policy language that works both in Kyverno and in native Kubernetes admission APIs, reducing cognitive overhead. Gatekeeper has had mutation (`Assign`, `AssignMetadata`, `ModifySet`, `AssignImage`) as a stable feature since v3.10, so mutation is no longer a differentiator. What Gatekeeper still has no equivalent for is Kyverno's `generate` rule — policy-driven creation of dependent resources such as NetworkPolicies or ResourceQuotas on namespace creation — which is why Kyverno remains the better fit when you need that generation path.
What is the difference between validate.failureAction: Audit and Enforce?
In Audit mode, policy violations are recorded in PolicyReport CRDs but the admission request is allowed through. In Deny/Enforce mode, violations block the admission request with the policy's message returned to the user. On `ValidatingPolicy`, that switch is `validationActions: [Audit]` vs `[Deny]`. On legacy ClusterPolicy, prefer per-rule `validate.failureAction` over the deprecated top-level `spec.validationFailureAction`. Always introduce new policies in Audit first, let the background controller run a full scan cycle, review the violations, remediate or create exceptions for legitimate cases, and only then switch to Deny/Enforce. Skipping the Audit phase on a busy cluster will cause immediate deployment failures that surface as an emergency rather than a planned change.
How do I prevent Kyverno's own admission webhook from becoming a single point of failure?
Run a minimum of three Kyverno replicas in production. Set `spec.webhookConfiguration.timeoutSeconds` to a conservative value (default 10s; valid range 1–30s) and configure `failurePolicy: Fail` only for your most critical policies — accepting that a Kyverno outage will block those specific resource types. For less critical policies, `failurePolicy: Ignore` allows requests through during a Kyverno disruption. Use PodDisruptionBudgets to prevent all Kyverno pods from being evicted simultaneously during node maintenance. Monitor Kyverno webhook latency as a P99 metric — spikes above 100ms indicate evaluation performance problems that precede availability issues.
How does Kyverno integrate with ArgoCD and Flux for GitOps delivery?
Policy CRDs (`policies.kyverno.io/v1` types such as `ValidatingPolicy`, plus any remaining legacy `ClusterPolicy`) are standard Kubernetes resources and are reconciled by both ArgoCD and Flux like any other manifest. The recommended pattern is a dedicated policy repository separate from application repositories, managed by the platform team. Flux's OCI source support lets you publish policy bundles to a registry and have clusters pull them by digest — providing immutability guarantees. ArgoCD's ApplicationSet controller can fan out a single policy App definition to multiple clusters. In both cases, the critical gate is the CI pipeline that requires a same-directory `kyverno-test.yaml` for every policy path and runs `kyverno test` before any policy change reaches the main branch.
How do PolicyReport CRDs map to NIS2 and DORA audit requirements?
[NIS2 requires demonstrable technical controls with evidence of continuous operation. DORA requires ICT risk management controls with audit trails.](/blog/nis2-dora-eu-ai-act-self-hosted-kubernetes-compliance) PolicyReport CRDs, when stored in an append-only audit log with timestamps and resource UIDs, satisfy both requirements for the admission-control layer: they demonstrate that a specific control was enforced against a specific resource at a specific time. The key is immutability of the audit record — write PolicyReport data to append-only object storage with retention policies matching your compliance framework's evidence requirements (typically 3-7 years for regulated industries).
What is the right scope for a PolicyException and how should it be approved?
First enable the feature: admission controller `--enablePolicyException=true` and `--exceptionNamespace=<ns|*>` (off by default — applying a CRD alone is a silent no-op). Match the PolicyException API to the policy API: for `ValidatingPolicy` / other `policies.kyverno.io/*` types use `policies.kyverno.io` PolicyException with `policyRefs` + CEL `matchConditions`; for legacy `ClusterPolicy` only, use `kyverno.io/v2` with `exceptions[].policyName` / `ruleNames`. Cross-wiring them is a silent non-exemption. Keep each exception as narrow as possible: specific namespace, labels or CEL match, and named policy. Include an expiry date no more than 90 days out (requiring active renewal), a justification that references a ticket or vendor commitment, and an owner team accountable for remediation. Require a pull request review from the policy-owning team — not a Slack approval. The Git merge is the audit record. Exceptions without expiry dates are not exceptions; they are permanent policy carve-outs that will not be revisited.
Further reading
- NIST SP 800-53 on Kubernetes You Own: AC, AU, CM, SC, SI
- Default deny, actually: auditing Kubernetes network policy
- Distroless without a vendor: building your own base images
- ITAR: export-controlled workloads and US-person access
- GDPR Article 32: Technical Measures an Auditor Can Verify
- MeitY empanelment: architecting for Indian government cloud
- Own the registry: Harbor and Zot for air-gapped image delivery
- Kubernetes upgrade debt: skew, deprecated APIs and safe paths
- Internal PKI with step-ca and cert-manager: Private ACME
- Digital Sovereignty: From Slogan to Testable Architecture
- DPDP Compliance: Consent, Erasure and Breach Workflows
- DPDP Act for Engineers: India's Data Residency Architecture
- Hard multi-tenancy with vCluster: the isolation boundary policy can't draw
- Progressive delivery with Argo Rollouts: canary and blue-green
- Kubernetes Security Posture Management (KSPM) service deep-dive
- KubeVigil: Independent Kubernetes Audit and Compliance Scanning
- Worked scenario: regulated fintech cloud exit and governance build-out
- Why golden paths need a policy layer
- Image-signature verification with verifyImages
- Policy-as-code as audit evidence
- DORA's demand for dated evidence — and how PolicyReport CRDs supply it automatically
- EU AI Act High-Risk Systems: An On-Prem Compliance Path
- Distributing policy across clusters
- Runtime detection that complements admission control
- WebAssembly on Kubernetes: SpinKube and runwasi
- Owning the SOC 2 evidence trail: Trust Services Criteria on Kubernetes
- Annex A 8.9: policy reports as ISO 27001 configuration evidence
- TISAX ISA2027 for Suppliers Who Host Their Own Systems
- Secure by Design Is Now a Procurement Question
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.