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 ValidatingAdmissionPolicy changes 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 verify and kyverno test both 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 failurePolicy deliberately.

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:

  1. Mutating admission runs first. Webhooks may return a JSON patch that rewrites the object — injecting a sidecar, defaulting a resource limit, adding a label.
  2. 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.
  • failurePolicyFail means an unreachable or timing-out webhook blocks the write; Ignore means the write proceeds unchecked. Fail is 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 PolicyReport resources, 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 Config resource. 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, and ModifySet CRDs. 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 overlay pattern, anyPattern, deny conditions, or CEL expressions. Wildcards and anchors do the heavy lifting: ?* means "any non-empty value", !*:latest means "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 default NetworkPolicy, a ResourceQuota, and an image-pull Secret, with synchronize: true keeping 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 failurePolicy deliberately. Fail is correct for security-critical rules and means a policy outage becomes a deployment outage. Ignore keeps the cluster writable and silently stops enforcing. Many teams run Fail for a small critical set and Ignore for 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 Config sync. 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_seconds as 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:

  1. 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.
  2. The add-on excludes system namespaces (including kube-system and the Arc namespaces) from evaluation by default. Do not assume full coverage.
  3. 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:

  1. Inventory the source policies and, more importantly, the intents behind them. Most estates find duplicates and dead rules at this step.
  2. Author the target policies in audit mode (enforcementAction: dryrun for Gatekeeper, failureAction: Audit for Kyverno) and install alongside the incumbent. Two engines can coexist; both are just webhooks.
  3. 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.
  4. Cut over per namespace, starting with non-production, flipping the new engine to enforce while the old one drops to audit.
  5. 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: Fail with 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 Pod rejects the ReplicaSet's Pod creation, not the user's kubectl 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 initContainers and ephemeralContainers. A limits policy that only checks spec.containers is trivially bypassed and will pass every review that only reads the happy path.
  • Not excluding system namespaces. kube-system and 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 verify and kyverno test exist. 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 verify or kyverno 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

  1. Inventory what you already run. List every validating and mutating webhook configuration in each cluster with the kubectl commands above, and record the failure policy and timeout for each.
  2. 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.
  3. Check whether Pod Security Admission is enough. If baseline or restricted profiles cover your needs, three namespace labels beat installing a policy engine.
  4. 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.
  5. Stand up policy tests in CI on day one — gator verify or kyverno test against fixture manifests, wired into the same pipeline that lints your charts.
  6. Deploy in audit mode for a full release cycle, export the findings, and fix workloads before flipping a single rule to enforce.
  7. 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

Thorsteinn Halldorsson Senior Cloud Engineer

Senior Cloud Engineer with 25+ years of hands-on experience across the datacenter-to-cloud stack: fiber SAN and disk storage, IBM/Lenovo blade and Dell/HP/Lenovo servers, Hyper-V and VMware clusters, and SQL and Remote Desktop Services (RDS) clusters. Deep in the Microsoft platform — Active Directory, PKI/certificate services, SQL, Power BI, Dynamics 365 Business Central (NAV) and AX (Axapta), Microsoft 365, Entra, and Intune — with a focus on Azure operations, FinOps, and applying AI tools like GitHub Copilot and Claude in real workflows. Writes practical, no-nonsense guides for IT professionals who need to ship real solutions.

Leave a Reply

Your email address will not be published. Required fields are marked *