Executive Snapshot
| Dimension | OPA Gatekeeper | Kyverno |
|---|---|---|
| Policy language | Rego, wrapped in a ConstraintTemplate CRD | YAML, with JMESPath expressions and a CEL-based policy type in newer releases |
| Policy shape | Two objects: reusable ConstraintTemplate + parameterised Constraint |
One object: a ClusterPolicy (or namespaced Policy) containing rules |
| Validation | Yes, via validating webhook | Yes, via validating webhook |
| Mutation | Yes, via Assign/AssignMetadata/ModifySet CRDs — declarative, no Rego |
Yes, via mutate rules inside the same policy, including mutate-on-existing |
| Generation of new resources | No | Yes — generate rules create and sync downstream objects |
| Cleanup of resources | No | Yes — CleanupPolicy / TTL-based deletion |
| Image verification | Not built in | Yes — verifyImages with Sigstore/Cosign |
| Testing CLI | gator test / gator verify, plus opa test and Konstraint for Rego unit tests |
kyverno apply and kyverno test against fixture files |
| Reporting | Violations on the Constraint .status, capped; PolicyReport via exporters |
Native PolicyReport / ClusterPolicyReport custom resources |
| Managed Azure story | Azure Policy for AKS is Gatekeeper-based — first-party add-on, built-in initiatives, compliance in Defender for Cloud | Self-managed install (Helm); no Azure Policy compliance integration |
| Learning curve | Steep — Rego is a genuinely different language | Shallow for validation, steeper once you hit JMESPath and context variables |
| Best for | Regulated estates that want Azure Policy compliance reporting, or teams already invested in Rego across the stack | Platform teams who want Kubernetes-native YAML, mutation, and generation without learning a new language |
Both projects are CNCF-hosted and both are in production at large scale. Everything about relative maturity below is a snapshot: verify feature stage, supported Kubernetes versions, and resource guidance against the current upstream docs before you commit a platform decision to either one.
TL;DR
- The real difference is authoring model, not capability. Gatekeeper asks you to learn Rego and split every policy into a template plus a constraint. Kyverno asks you to write YAML that looks like the resource you are validating. Both block the same bad Pods.
- Kyverno does more than validate. Mutation, resource generation, cleanup, and image signature verification live in one engine. Gatekeeper does validation and mutation; the other two are out of scope by design.
- Azure Policy for AKS is Gatekeeper. If you need AKS policy compliance visible in Azure Policy and Defender for Cloud, that lane is Gatekeeper-shaped, and running your own second Gatekeeper alongside the add-on is not supported.
- Built-in
ValidatingAdmissionPolicychanges the floor, not the ceiling. CEL-based validation went GA in Kubernetes 1.30 and removes the webhook hop for simple rules — but it has no audit engine, no reporting CRs, no mutation story at GA, and no policy library. Both tools now emit or author CEL rather than compete with it. - Test policies before they reach a cluster.
gator verifyandkyverno testboth run policies against fixture manifests in CI. A policy that has never been tested against a real manifest is an outage waiting for a Friday. - Latency and failure policy are the operational risk. Every admission webhook sits in the critical path of every write. Set timeouts, scope your matches, exclude system namespaces, and decide
failurePolicydeliberately.
Introduction
Admission control is where a Kubernetes cluster stops being a suggestion box. RBAC decides who can create a Pod; admission control decides whether that specific Pod is acceptable. It is the only layer that can reject a manifest for having no memory limit, no non-root user, no image tag, or a hostPath mount into /etc.
Kubernetes ships the plumbing — ValidatingAdmissionWebhook and MutatingAdmissionWebhook — but not the policies. That gap is what OPA Gatekeeper and Kyverno fill, and they fill it in two philosophically different ways.
This article covers what admission control actually does (including how the newer built-in CEL policy types change the calculus), how each project is architected, the same two rules implemented in both engines, and a head-to-head on the dimensions that decide real adoptions: language, mutation, testing, scale behaviour, reporting, the Azure integration, and migration.
Admission Control Fundamentals
An API request that survives authentication and authorisation enters the admission chain. Two webhook phases matter here:
- Mutating admission runs first. Webhooks may return a JSON patch that rewrites the object — injecting a sidecar, defaulting a resource limit, adding a label.
- Validating admission runs after mutation and after schema validation. Webhooks may only say yes or no. Nothing may change the object at this point, which is why a validating policy sees the final object.
Three configuration fields on a ValidatingWebhookConfiguration decide the blast radius, and both tools expose them:
rules— which API groups, versions, resources, and operations trigger the call. Narrow this aggressively.failurePolicy—Failmeans an unreachable or timing-out webhook blocks the write;Ignoremeans the write proceeds unchecked.Failis the secure default and the one that takes clusters down when the policy pods are unhealthy.timeoutSeconds— capped at 30, but anything above a few seconds on a hot path is already a problem.
# See every admission webhook in the cluster, with its failure policy and timeout.
kubectl get validatingwebhookconfigurations \
-o custom-columns='NAME:.metadata.name,WEBHOOKS:.webhooks[*].name,FAILURE:.webhooks[*].failurePolicy,TIMEOUT:.webhooks[*].timeoutSeconds'
kubectl get mutatingwebhookconfigurations \
-o custom-columns='NAME:.metadata.name,FAILURE:.webhooks[*].failurePolicy,TIMEOUT:.webhooks[*].timeoutSeconds'
The CEL evolution, and what it changes
Kubernetes now has an in-process alternative to webhooks. ValidatingAdmissionPolicy — validation expressed in CEL (Common Expression Language), evaluated inside the API server with no network hop — graduated to GA in Kubernetes 1.30. A mutating equivalent, MutatingAdmissionPolicy, arrived later and has been progressing through the alpha/beta stages; confirm its exact stage and whether the feature gate is enabled on your cluster version before planning around it, particularly on managed control planes like AKS where feature gates are not yours to flip.
What this genuinely removes: the latency, the certificate rotation, the failurePolicy cliff, and the "policy pods are down so nothing can deploy" incident class — for rules simple enough to express as a CEL expression over the incoming object.
What it does not remove:
- Background audit. CEL policies evaluate on write. They do not tell you which of the 4,000 Pods already running would fail the rule.
- Reporting. There are no
PolicyReportresources, no violation history, no dashboard. - Policy libraries. Gatekeeper ships a substantial community library; Kyverno ships a large curated policy catalogue. Raw CEL ships expressions you write yourself.
- Anything beyond validate. Generation, cleanup, and image signature verification remain out of scope.
Both projects treated CEL as an opportunity rather than a threat. Gatekeeper can generate ValidatingAdmissionPolicy objects from ConstraintTemplates that carry a K8sNativeValidation engine block, with generateVAP: true opting a template in — so a subset of your constraints execute in-process while the audit and reporting machinery stays. Kyverno added CEL expressions inside classic rules and, in newer releases, a CEL-native policy type alongside the original ClusterPolicy. Treat the maturity of both paths as version-dependent and verify against current docs.
The honest summary: built-in CEL is becoming the right execution engine for simple validation, and Gatekeeper and Kyverno are becoming the right authoring, audit, and reporting layer above it. That is a strong argument for adopting a policy engine now rather than hand-rolling CEL and rebuilding the surrounding tooling yourself.
Gatekeeper Architecture
Gatekeeper is OPA packaged for Kubernetes admission. Policy logic is Rego; the Kubernetes-facing wrapper is two CRDs.
ConstraintTemplate defines a new constraint kind and the Rego that implements it. It declares an OpenAPI v3 schema for the parameters the policy accepts, which is how the same logic gets reused with different arguments.
Constraint is an instance of that kind — the concrete "apply this, to these resources, in these namespaces, with these parameters" object. The API group is constraints.gatekeeper.sh, and the kind is whatever the template declared.
Three more pieces matter operationally:
- Audit runs on an interval, re-evaluating existing cluster resources against every constraint and writing violations into the constraint's
.status.violations. The list is capped (--constraint-violations-limit), so a constraint that fires against hundreds of workloads shows a truncated view — read the total, not the array length. - Data replication via the
Configresource. Rego that needs to reason about other objects (uniqueness of an ingress host, for example) requires those objects to be synced into OPA's cache first. Sync too much and you pay for it in memory. - Mutation through separate
Assign,AssignMetadata,AssignImage, andModifySetCRDs. These are declarative — no Rego involved — which is a pleasant surprise for anyone expecting to write mutation logic in a query language.
enforcementAction on a constraint accepts deny, dryrun, and warn. dryrun records violations without blocking, which is the only sane way to introduce a policy to an existing cluster.
Kyverno Architecture
Kyverno's design premise is that Kubernetes administrators already read and write YAML all day, so policy should be YAML too — and a validation rule should look like the resource it validates.
A ClusterPolicy (cluster-scoped) or Policy (namespaced) contains a list of rules. Each rule has a match/exclude selector and exactly one action block:
validate— accept or reject, using an overlaypattern,anyPattern,denyconditions, or CEL expressions. Wildcards and anchors do the heavy lifting:?*means "any non-empty value",!*:latestmeans "must not end in:latest",+(field)adds a value only when absent,(field)marks a conditional anchor.mutate— a strategic merge patch or JSON patch applied at admission, and optionally to already-existing resources (mutateExistingOnPolicyUpdate).generate— create downstream objects when a trigger appears. New namespace gets a defaultNetworkPolicy, aResourceQuota, and an image-pullSecret, withsynchronize: truekeeping them reconciled.verifyImages— check container image signatures and attestations against Sigstore/Cosign keys or keyless identities, and optionally mutate the image reference to its resolved digest.CleanupPolicy— a separate resource type that deletes matching objects on a schedule or TTL.
Enforcement is per-rule. The field is spec.rules[*].validate.failureAction, valued Enforce or Audit; the older cluster-wide spec.validationFailureAction is deprecated in current releases, and you will still see it throughout older blog posts and Stack Overflow answers. Prefer the rule-level field for new policies.
Kyverno also splits its controllers — admission, background/reports, cleanup — into separate deployments so the write path is not competing with report generation for CPU. That split matters at scale and is covered below.
The Same Two Rules, Both Engines
The rules: every container must declare CPU and memory limits, and no container may use the :latest tag or an untagged image.
Gatekeeper
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sworkloadhygiene
spec:
crd:
spec:
names:
kind: K8sWorkloadHygiene
validation:
openAPIV3Schema:
type: object
properties:
exemptImages:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sworkloadhygiene
# --- resource limits ---------------------------------------------
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not c.resources.limits.cpu
msg := sprintf("container <%v> is missing resources.limits.cpu", [c.name])
}
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not c.resources.limits.memory
msg := sprintf("container <%v> is missing resources.limits.memory", [c.name])
}
# --- image tag ----------------------------------------------------
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not exempt(c.image)
endswith(c.image, ":latest")
msg := sprintf("container <%v> uses the mutable tag :latest", [c.name])
}
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
not exempt(c.image)
not tagged(c.image)
msg := sprintf("container <%v> has no tag, which resolves to :latest", [c.name])
}
exempt(image) {
prefix := input.parameters.exemptImages[_]
startswith(image, prefix)
}
# Naive tag detection: split off any registry host that carries a port
# (registry.example.com:5000/app) before looking for the tag separator.
tagged(image) {
parts := split(image, "/")
last := parts[count(parts) - 1]
contains(last, ":")
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sWorkloadHygiene
metadata:
name: workload-hygiene
spec:
enforcementAction: dryrun # flip to deny once audit is clean
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
exemptImages:
- "mcr.microsoft.com/oss/kubernetes/"
That tagged helper is the whole Rego learning curve in miniature. The obvious implementation — "does the image string contain a colon?" — silently passes every image pulled from a registry with an explicit port. Rego will not warn you; only a test fixture will. Note also that this template only inspects spec.containers; a production version needs initContainers and ephemeralContainers too, and needs to decide whether it matches bare Pods or the workload controllers that create them.
Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: workload-hygiene
spec:
background: true
rules:
- name: require-resource-limits
match:
any:
- resources:
kinds: ["Pod"]
exclude:
any:
- resources:
namespaces: ["kube-system", "kyverno"]
validate:
failureAction: Audit # switch to Enforce after review
message: "Every container must set resources.limits.cpu and resources.limits.memory."
pattern:
spec:
containers:
- (name): "*"
resources:
limits:
cpu: "?*"
memory: "?*"
"=(initContainers)":
- "=(name)": "*"
resources:
limits:
cpu: "?*"
memory: "?*"
- name: require-image-tag
match:
any:
- resources:
kinds: ["Pod"]
validate:
failureAction: Audit
message: "Images must carry an explicit tag."
pattern:
spec:
containers:
- image: "*:*"
- name: disallow-latest-tag
match:
any:
- resources:
kinds: ["Pod"]
validate:
failureAction: Audit
message: "The mutable tag ':latest' is not permitted."
pattern:
spec:
containers:
- image: "!*:latest"
Same enforcement, no new language, and the pattern block is shaped like the Pod spec it is checking. Kyverno also auto-generates equivalent rules for Deployments, StatefulSets, CronJobs and friends when the rule matches Pods, so a rejection surfaces at kubectl apply time on the Deployment rather than silently inside a failing ReplicaSet — a genuinely useful default that Gatekeeper leaves to you.
Mutation: defaulting a missing limit
Gatekeeper, declaratively:
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: default-memory-limit
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
excludedNamespaces: ["kube-system", "gatekeeper-system"]
location: "spec.containers[name:*].resources.limits.memory"
parameters:
pathTests:
- subPath: "spec.containers[name:*].resources.limits.memory"
condition: MustNotExist
assign:
value: "512Mi"
Kyverno, as one more rule in the same policy:
- name: default-memory-limit
match:
any:
- resources:
kinds: ["Pod"]
mutate:
patchStrategicMerge:
spec:
containers:
- (name): "*"
resources:
limits:
+(memory): "512Mi"
The +(memory) anchor is "add this key only if it is absent" — the same intent as Gatekeeper's MustNotExist path test, in four fewer lines and one fewer CRD. Whichever engine you pick, remember that mutating defaults into workloads makes your cluster's behaviour differ from what is in Git, so keep them few, documented, and visible.
Head to Head
Language and learning curve
Rego is a declarative query language with its own evaluation model — partial rules, set comprehensions, unification, and the "everything is a query over input" mental model. It is genuinely powerful and genuinely unlike anything most platform engineers already know. The cost is real: expect a couple of weeks before an engineer writes correct non-trivial Rego unaided, and expect that engineer to become a bottleneck.
Kyverno's YAML is readable on day one for validation. The curve arrives later, at JMESPath expressions, context blocks that call the API server or an external service, preconditions, and variable substitution — that layer is not trivial, and its debugging story is weaker than Rego's, where opa eval gives you a proper REPL.
Fair summary: Kyverno's first ten policies are far easier; policy fifty is comparably hard in both.
Policy testing
Both projects can gate policies in CI against fixture manifests. This is the single highest-value practice in this entire article.
# Gatekeeper: evaluate templates + constraints against manifests
gator test --filename=policies/ --filename=fixtures/
# Gatekeeper: run declarative Suite/Test/Case fixtures with expected outcomes
gator verify ./policies/...
# Rego unit tests, independent of Kubernetes
opa test policies/ -v
# Konstraint: generate ConstraintTemplates + Constraints from plain .rego files
konstraint create policies/ --output ./generated
# Kyverno: apply policies to manifests and print per-rule results
kyverno apply policies/ --resource fixtures/ --detailed-results
# Kyverno: run declarative test fixtures (kyverno-test.yaml) with expected results
kyverno test ./tests/
A minimal Kyverno test fixture, which is what makes the CLI a regression suite rather than a linter:
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
name: workload-hygiene-test
policies:
- ../policies/workload-hygiene.yaml
resources:
- resources.yaml
results:
- policy: workload-hygiene
rule: require-resource-limits
resources: [pod-no-limits]
kind: Pod
result: fail
- policy: workload-hygiene
rule: disallow-latest-tag
resources: [pod-latest-tag]
kind: Pod
result: fail
- policy: workload-hygiene
rule: require-resource-limits
resources: [pod-good]
kind: Pod
result: pass
Konstraint deserves a specific mention on the Gatekeeper side: it lets you keep policies as plain .rego files with normal opa test unit tests and generate the ConstraintTemplate and Constraint YAML, which removes the worst ergonomic problem in Gatekeeper — Rego embedded as a string inside YAML, where no editor helps you.
Performance and behaviour at scale
Both engines add a network round trip to every matching API write. The controls are broadly the same, and they matter more than the engine choice:
- Scope the match. Do not match
["*"]on all resources. Match the kinds you actually police, and use namespace selectors to exclude system namespaces. - Set
failurePolicydeliberately.Failis correct for security-critical rules and means a policy outage becomes a deployment outage.Ignorekeeps the cluster writable and silently stops enforcing. Many teams runFailfor a small critical set andIgnorefor the long tail. - Run the admission controller HA. Multiple replicas, spread across nodes and zones, with a PodDisruptionBudget so a node drain cannot take the whole policy layer with it.
- Watch memory, not just CPU. Gatekeeper's memory tracks whatever you replicate into OPA's cache via the
Configsync. Kyverno's reports controller is the component that grows with cluster size, which is exactly why newer versions split it into its own deployment — size it separately from the admission path. - Measure the latency you actually add. The API server exposes
apiserver_admission_webhook_admission_duration_secondsas a histogram, labelled per webhook. That metric, not a benchmark blog post, is the number to argue from.
# P99 admission latency per webhook, from an API server metrics scrape.
# (Run against Prometheus; on managed control planes scrape via your metrics stack.)
histogram_quantile(
0.99,
sum by (name, le) (
rate(apiserver_admission_webhook_admission_duration_seconds_bucket[5m])
)
)
Published benchmarks comparing the two engines exist, and they disagree with each other, because results depend entirely on policy complexity, matched resource breadth, replica count, and cluster churn. Run the measurement on your own workload before treating throughput as a differentiator.
Reporting
Kyverno emits PolicyReport (namespaced) and ClusterPolicyReport (cluster-scoped) custom resources implementing the Kubernetes Policy WG report API. Every rule evaluation — pass, fail, warn, skip, error — lands in a queryable object, which makes kubectl a reporting tool:
# Cluster-wide failure summary
kubectl get clusterpolicyreport -o wide
# Every failing result in one namespace, flattened
kubectl get policyreport -n production -o json \
| jq -r '.items[].results[] | select(.result=="fail")
| [.policy, .rule, (.resources[0].kind + "/" + .resources[0].name)] | @tsv'
Gatekeeper reports violations on each constraint's .status.violations, which is convenient but capped by --constraint-violations-limit and shaped per-constraint rather than per-resource. Exporting Gatekeeper results into the same PolicyReport format is possible through the ecosystem — Policy Reporter, for instance, presents both engines in one UI — but it is an add-on rather than a native output.
If "show the auditors a per-namespace compliance report" is a hard requirement and you are not using Azure Policy, Kyverno gets there with less assembly.
Ecosystem and the Azure angle
This is where the decision often stops being technical.
Azure Policy for AKS is built on Gatekeeper v3. Enabling the Azure Policy add-on installs a managed Gatekeeper into the cluster; Azure Policy definitions carry ConstraintTemplates and parameters, and the resulting compliance state rolls up into Azure Policy and Microsoft Defender for Cloud alongside the rest of your Azure estate. Built-in initiatives cover the common baselines — Pod Security-style restrictions, allowed image registries, privileged container prevention, resource limits — and custom definitions let you ship your own ConstraintTemplates through the same pipeline.
Three practical consequences:
- You do not install Gatekeeper yourself when using the add-on. Running a self-managed Gatekeeper alongside the Azure Policy add-on is not a supported configuration, and the two will fight over the same CRDs. Pick one.
- The add-on excludes system namespaces (including
kube-systemand the Arc namespaces) from evaluation by default. Do not assume full coverage. - Enforcement is Azure-shaped. Definitions are assigned at management group, subscription, or resource group scope with an effect (
audit,deny,disabled), and compliance evaluation runs on Azure's cadence — not instantly after every write.
Kyverno on AKS is a normal Helm install and behaves identically to Kyverno anywhere else, but nothing it does appears in Azure Policy compliance. If your governance reporting line is Azure Policy, that is a structural gap you have to fill with a separate dashboard.
Both projects are CNCF-hosted, both have active communities, both ship substantial policy libraries. Gatekeeper carries the advantage of Rego skills transferring to non-Kubernetes OPA use cases — API authorisation, Terraform plan checks, CI gates — which is a real argument if you are standardising a policy language across the platform rather than just the cluster.
Migration between them
There is no automated translator, and you should not want one — a mechanically converted policy is a policy nobody understands. The intents map cleanly, though, so migration is a re-authoring exercise with a safe pattern:
- Inventory the source policies and, more importantly, the intents behind them. Most estates find duplicates and dead rules at this step.
- Author the target policies in audit mode (
enforcementAction: dryrunfor Gatekeeper,failureAction: Auditfor Kyverno) and install alongside the incumbent. Two engines can coexist; both are just webhooks. - Diff the findings. Compare the new engine's audit results against the old engine's violations on the same live cluster. Any resource that one flags and the other does not is a translation bug — investigate before proceeding.
- Cut over per namespace, starting with non-production, flipping the new engine to enforce while the old one drops to audit.
- Remove the incumbent only after a full release cycle with zero enforcement divergence.
The same pattern applies to adopting either engine for the first time on an existing cluster, with step 3 becoming "read the audit results and fix the workloads."
The Decision Framework
flowchart TD
A[Do you need AKS compliance in Azure Policy or Defender for Cloud] -->|Yes| B[Gatekeeper via the Azure Policy add-on]
A -->|No| C[Do you need generate, cleanup, or image verification]
C -->|Yes| D[Kyverno]
C -->|No| E[Is Rego already used elsewhere in the platform]
E -->|Yes| F[Gatekeeper]
E -->|No| G[Does the team have Rego capacity to build and maintain]
G -->|No| D
G -->|Yes| H[Either — pick on reporting and ecosystem fit]
Regulated Azure estate, central governance team. Gatekeeper through the Azure Policy add-on. The compliance rollup into Defender for Cloud, assignment at management group scope, and first-party support are worth more than authoring ergonomics.
Platform team building an internal developer platform, small and busy. Kyverno. generate rules that stamp every new namespace with a default NetworkPolicy, ResourceQuota, and pull secret remove a whole category of onboarding toil that Gatekeeper simply does not address.
Organisation already running OPA for API authorisation or CI gates. Gatekeeper. One language, one review process, one set of test tooling across the platform is a genuine multiplier.
Supply chain security is the driver. Kyverno. verifyImages with Cosign, plus digest mutation, is native; achieving the equivalent under Gatekeeper means adding another component (Sigstore's policy-controller or similar) alongside it.
Multi-cloud or multi-distribution fleet with a central policy repo. Slight edge to Gatekeeper for language portability, but this is a genuine toss-up — decide on team skills, not architecture.
Just want Pod Security Standards enforced. Neither, first. Built-in Pod Security Admission covers the baseline and restricted profiles with three namespace labels and zero components. Add a policy engine when you need rules PSA does not express.
Pitfalls to Avoid
- Going straight to enforce on an existing cluster. Start in
dryrun/Audit, read the findings, fix or exempt, then enforce. This one mistake causes most "the policy engine broke the cluster" stories. failurePolicy: Failwith a single replica. One evicted Pod and the cluster stops accepting writes for the matched resources. HA plus a PodDisruptionBudget is not optional.- Matching Pods but deploying Deployments. A rule that only matches
Podrejects the ReplicaSet's Pod creation, not the user'skubectl apply, so the failure surfaces as a Deployment that never scales with the reason buried in events. Kyverno auto-generates controller rules; with Gatekeeper, match the controllers explicitly. - Forgetting
initContainersandephemeralContainers. A limits policy that only checksspec.containersis trivially bypassed and will pass every review that only reads the happy path. - Not excluding system namespaces.
kube-systemand the engine's own namespace need exemptions, or an upgrade of a system component gets blocked by your own policy at the worst moment. - Treating policy as a substitute for RBAC. Admission control constrains what can be created; it does not constrain who can bypass it by editing the policy. Lock down write access to
ConstraintTemplate,ClusterPolicy, and the webhook configurations themselves. - Shipping policies without tests.
gator verifyandkyverno testexist. A policy repo without fixtures is untested code with cluster-wide blast radius. - Ignoring
apiserver_admission_webhook_admission_duration_seconds. Latency creep is invisible until a deploy storm makes it visible.
Troubleshooting
A policy is installed but nothing is being blocked
Check the enforcement mode first — enforcementAction on the Gatekeeper constraint, validate.failureAction on the Kyverno rule. dryrun/Audit is the most common answer. Next check background (Kyverno) and the match/exclude selectors, and confirm the namespace is not in an exclusion list. On the Azure Policy add-on, also confirm the assignment effect is deny rather than audit, and remember evaluation is not instantaneous.
Everything is rejected with failed calling webhook
The policy pods are unreachable, unhealthy, or their serving certificate has expired, and failurePolicy: Fail is doing exactly what it was configured to do. Check pod health and the webhook's CA bundle. As an emergency measure the webhook configuration can be patched to Ignore or deleted — deleting it disables enforcement entirely, so treat that as a break-glass action with a follow-up.
Gatekeeper constraint status shows fewer violations than expected
The list is capped by --constraint-violations-limit. Read status.totalViolations for the real count, or export findings rather than reading the array.
A Rego policy passes opa test but fails in the cluster
Almost always the input shape. Gatekeeper wraps the object as input.review.object, with input.review.oldObject on updates and parameters at input.parameters. Unit tests written against a bare object will not catch this — write fixtures at the AdmissionReview shape, or use gator test which supplies the real wrapper.
A Kyverno pattern matches nothing
Check anchors. (name): "*" is a conditional anchor, =(field) is an existence anchor, ?* is "any non-empty value", and a plain key is an exact match. A missing anchor on a list element usually means the pattern is being applied to only the first array entry rather than all of them. Run kyverno apply against a fixture to see per-rule results rather than guessing.
Deployments are rejected but the error is unhelpful
Look at the ReplicaSet events, not the Deployment. The message from the policy engine is attached to the failed Pod creation attempt: kubectl describe replicaset <name> and kubectl get events --sort-by=.lastTimestamp -n <ns>.
Key Takeaways
- Gatekeeper and Kyverno solve the same core problem; the durable difference is that Gatekeeper asks you to learn Rego and Kyverno asks you to write Kubernetes-shaped YAML.
- Kyverno's scope is wider — mutate, generate, cleanup, and image verification in one engine — while Gatekeeper stays focused on validation and declarative mutation.
- Azure Policy for AKS is Gatekeeper-based, and that single fact decides the choice for many Azure estates. Do not run your own Gatekeeper alongside the add-on.
- Built-in
ValidatingAdmissionPolicy(GA in Kubernetes 1.30) is displacing webhooks for simple validation, but it has no audit, reporting, or policy library — both engines are adapting to emit or author CEL rather than being replaced by it. - Test policies in CI with
gator verifyorkyverno test. Untested policy is the fastest route to a self-inflicted cluster outage. - Latency,
failurePolicy, replica count, and match scope are the operational risks that actually bite, and they are near-identical for both engines. - Roll out in audit mode, always. Read the findings, fix the workloads, then enforce.
Next Steps
- Inventory what you already run. List every validating and mutating webhook configuration in each cluster with the
kubectlcommands above, and record the failure policy and timeout for each. - Write down the five rules that actually matter to your organisation before evaluating either tool. Most teams discover that Pod Security Admission plus three custom rules covers the real requirement.
- Check whether Pod Security Admission is enough. If baseline or restricted profiles cover your needs, three namespace labels beat installing a policy engine.
- Prototype the same two rules in both engines using the manifests in this article, on a throwaway cluster, and have the team that will maintain them do the writing.
- Stand up policy tests in CI on day one —
gator verifyorkyverno testagainst fixture manifests, wired into the same pipeline that lints your charts. - Deploy in audit mode for a full release cycle, export the findings, and fix workloads before flipping a single rule to enforce.
- On AKS, decide the Azure Policy question explicitly. If compliance must appear in Defender for Cloud, enable the add-on and build on it. If not, you are free to choose on merit.
Related Articles
- AKS Production Hardening: A 2026 Security and Reliability Checklist — where admission policy fits in the broader cluster hardening sequence
- Kubernetes Ingress Controllers Compared: nginx vs Traefik vs Azure Application Gateway — the same comparison discipline applied to the ingress layer
- Zero Trust Network Architecture: A Practical Azure Implementation Guide — the policy-enforcement mindset above the cluster boundary
- Azure RBAC Best Practices: Lock Down Your Cloud Permissions in 2026 — authorisation is the layer admission control assumes is already correct
Leave a Reply