Admission control is the API server's last word. Policy engines are well-organized admission webhooks plus reporting. The lab runs three engines deliberately: the same rule in three dialects is the fastest way to learn what is engine-specific and what is admission-generic.
make up secOrientation
Get the request pipeline exact and most of this section is corollaries.
request ──▶ authn ──▶ authz (RBAC) ──▶ mutating admission ──▶ schema validation ──▶ validating admission ──▶ etcd
│ │
LimitRanger, sidecar and PSS, ValidatingAdmissionPolicy,
label injection, Kyverno Kyverno, Gatekeeper webhooks
MutatingPolicy (all see the mutated object)
Run a pod through it. The interesting cases are the ones where a mutation quietly satisfies a validation:
- Validating policies see the object after mutation, so a mutation can satisfy a validation the user never wrote (that is the LimitRange trap below).
- Admission applies at write time only. Tightening a policy never touches existing objects; you need an audit/report mechanism for those.
- Every webhook is an availability dependency of the API server, and
failurePolicychooses which way you fail.
The three dialects
| Aspect | Kyverno | Gatekeeper (OPA) | ValidatingAdmissionPolicy |
|---|---|---|---|
| Language | YAML + CEL | Rego | CEL |
| Objects | ClusterPolicy/Policy (classic), and ValidatingPolicy/MutatingPolicy/ImageValidatingPolicy (newer, CEL) | ConstraintTemplate → generated CRD → Constraint | ValidatingAdmissionPolicy + …Binding |
| Can mutate | yes | assign mutations (separate CRDs) | a Mutating counterpart exists in newer versions |
| Can generate | yes (create resources on events) | no | no |
| Reporting | PolicyReport CRs | violation counts on the Constraint, via audit sweeps | none built in |
| Runs as | a webhook (a Deployment you must keep alive) | a webhook | in-process in the API server |
| Enforcement dial | classic: spec.validationFailureAction: Audit|Enforce · newer: spec.validationActions: [Audit|Deny|Warn] | enforcementAction: deny|warn|dryrun | validationActions on the binding |
Kyverno speaks two dialects, and you need both. The classic one is ClusterPolicy/Policy: a list of typed rules (validate, mutate, generate, verifyImages) with match/exclude blocks and a JMESPath-flavored pattern language. It switches between reporting and enforcement via spec.validationFailureAction: Audit|Enforce. It is fully supported and it is what most installed Kyverno runs. The newer dialect (ValidatingPolicy, MutatingPolicy, ImageValidatingPolicy, CEL expressions, spec.validationActions: [Audit|Deny|Warn]) arrived in Kyverno 1.14/1.15 and is what this lab installs. You do not get to choose which one the exam's cluster has, so recognize both, and let kubectl api-resources | grep kyverno plus kubectl explain settle which is in front of you. The lab's examples/kyverno/ pair is the newer form: require-resources.yaml validates that every container declares requests (in Audit mode; flip to Deny to enforce), and add-tenant-label.yaml mutates a cost-centre label onto Deployments at admission.
Gatekeeper is two-step by design: a ConstraintTemplate defines a parameterized policy in Rego and generates a CRD; a Constraint is an instance of that CRD binding it to resources with parameters. That indirection is what makes a library of reusable policies possible, and the gatekeeper-library repo is that library. This is also where the exam's "OPA" lives in practice: Rego inside templates.
ValidatingAdmissionPolicy is native: CEL, no controller to install, no webhook to keep alive, paired with a Binding that says which namespaces and what action. Worth one rep because it is what the other two increasingly compile down to, and because a task could hand it to you.
The rollout choreography is a better exam answer than "apply the policy": ship in Audit, read the reports to find every existing offender, fix them (or exempt them explicitly), then flip to Deny. Kyverno's PolicyReports and Gatekeeper's audit violations exist for exactly that middle step, and the same staged posture appears in PSS (5.3) as warn/audit before enforce.
Kyverno by version and by field
Which Kyverno is in front of you
| Kind | API | Since | Status |
|---|---|---|---|
| ValidatingPolicy, ImageValidatingPolicy | policies.kyverno.io/v1 | 1.14 (Apr 2025) | stable since 1.18 (Apr 2026); v1alpha1 deprecated |
| MutatingPolicy, GeneratingPolicy, DeletingPolicy | policies.kyverno.io/v1 | 1.15 (Jul 2025) | stable since 1.18 |
| Namespaced* variants | policies.kyverno.io/v1 | with the above | same policy, scoped to one namespace |
| PolicyException | policies.kyverno.io/v1 | 1.14 | stable since 1.19; the kyverno.io one is deprecated |
| ClusterPolicy / Policy, CleanupPolicy | kyverno.io/v1, kyverno.io/v2 | the classic API | deprecation marked 1.17 (Feb 2026), bug fixes only 1.18, deprecated 1.19 (Aug 2026), removal planned 1.20 (about Nov 2026) |
From 1.19 creating a classic policy returns an admission warning and increments kyverno_deprecated_api_requests_total; kubectl api-resources | grep kyverno and the warnings tell you which generation the exam cluster runs. Both still work in 2026, and an exam task may hand you either; recognize the fields of each.
CEL-based policy fields
spec.matchConstraints(resourceRules[].apiGroups/apiVersions/operations/resources, plusnamespaceSelector/objectSelector) andspec.matchConditions(CEL pre-filters) are copied from the Kubernetes VAP API.spec.variablesname CEL expressions once;spec.validations[]holdexpression,messageormessageExpression.spec.validationActions:Deny,Audit,Warn(a list; the lab flips["Audit"]to["Deny"]).spec.failurePolicyFail(default) orIgnorecovers evaluation errors and webhook timeouts.spec.evaluation:admission.enabledandbackground.enabledswitch the two engines independently (a policy with admission off is report-only);mode: JSONlets the same policy check a Terraform plan or any payload with the CLI.spec.webhookConfiguration.timeoutSeconds: default 10, range 1-30; Kyverno writes it into the generatedValidatingWebhookConfiguration.spec.autogen:podControllers.controllers: [deployments, statefulsets, ...]rewrites a Pod rule so it also matches templates at admission (the answer to "the policy only fires on pods, not on the Deployment");validatingAdmissionPolicy.enabled: truecompiles the policy into a native VAP so the API server enforces it in-process. The two are mutually exclusive; with pod-controller autogen on, Kyverno skips VAP generation and says so in the policy status.- MutatingPolicy adds
mutations[]withpatchType: ApplyConfiguration(a server-side-apply style partial object built by CEL) orJSONPatch, plusevaluation.mutateExisting.enabledfor retrofitting objects that already exist. GeneratingPolicy usesgenerate[].expression: generator.Apply(namespace, [objects])or YAMLtemplateblocks, withevaluation.synchronize,generateExistingandorphanDownstreamOnPolicyDelete. DeletingPolicy has a cronschedule,conditionsanddeletionPropagationPolicy.
Classic ClusterPolicy fields, still worth reading
A rule is match.any[]/exclude.any[] blocks of resources.kinds, namespaces, selector, subjects, then one of validate (pattern, anyPattern, deny.conditions, cel.expressions, podSecurity), mutate (patchStrategicMerge, patchesJson6902, targets for existing objects), generate (data or clone, synchronize) or verifyImages. Enforcement is validate.failureAction: Audit|Enforce per rule (the older spec.validationFailureAction and spec.webhookTimeoutSeconds, spec.failurePolicy are deprecated since 1.13 and the default action is Audit, so a copied sample policy never blocks anything until you change it). spec.background: true (default) drives report generation for existing objects. Pattern anchors: =(field) conditional, +(field) add if missing, X(field) negation, <(field) global, wildcards "*" and "?*".
Exceptions, reports, CLI, configuration
- PolicyException (
policies.kyverno.io/v1):policyRefs[].name/kindplusmatchConditionsin CEL; works in admission and background and shows up in the report as askipwith the exception's name. Exceptions are off until the controllers run with--enablePolicyException=trueand--exceptionNamespace=<ns>(Helm values of the same names); a task that says "exempt this workload" fails silently if that flag is off. - Reports.
PolicyReport(namespaced) andClusterPolicyReportinwgpolicyk8s.io/v1alpha2, one per resource, withresults[].resultinpass|fail|warn|error|skipand asummary. Generated by admission events and by the background scan in the reports controller;--allowedResults=fail(1.17+) stores only failures to keep etcd small. Enforced (Deny) failures are not reported because the object never existed. - CLI.
kyverno apply policy.yaml --resource pod.yaml(or--cluster) prints pass/fail per rule;kyverno test .runs akyverno-test.yamldeclaring policies, resources and expectedresults[].resultper rule and resource, which is how policies get unit tests in CI;kyverno apply ... -o out/shows mutated output. The CLI warns on classic kinds and--warnings-as-errorsfails the run. - Configuration. The
kyvernoConfigMap'sresourceFilters([Kind,namespace,name]triples, wildcards allowed) exclude objects from admission;excludeGroupsdefaults tosystem:serviceaccounts:kube-system,system:nodesandexcludeUsernamesto!system:kube-scheduler. Kyverno's own namespace is excluded by default andkube-systemis not. The webhooks that matter arekyverno-resource-validating-webhook-cfgandkyverno-resource-mutating-webhook-cfg; deleting them is the documented emergency step when every write times out because the admission controller is down, and Kyverno recreates them when it recovers.--forceFailurePolicyIgnoreflips every generated webhook toIgnore.
Gatekeeper and the native policies, field by field
Gatekeeper
- ConstraintTemplate (
templates.gatekeeper.sh/v1):spec.crd.spec.names.kindbecomes the Constraint kind (K8sRequiredLabels),spec.crd.spec.validation.openAPIV3Schematypes theparameters, andspec.targets[].target: admission.k8s.gatekeeper.shcarries the code:regowith aviolation[{"msg": msg, "details": {...}}]rule overinput.review.objectandinput.parameters, orcode[].engine: K8sNativeValidationwith CELvalidations(andgenerateVAP: trueto have Gatekeeper emit a real ValidatingAdmissionPolicy). When both engines are present, CEL wins and Rego is ignored. - Constraint (
constraints.gatekeeper.sh/v1beta1, kind from the template):spec.matchwithkinds[].apiGroups/kinds,scope: Cluster|Namespaced|*,namespaces/excludedNamespaces(prefix globs likekube-*),labelSelector,namespaceSelector;spec.parameters;spec.enforcementAction: deny|warn|dryrun(defaultdeny) orscopedwithscopedEnforcementActions[]naming an action per enforcement point (validation.gatekeeper.shthe webhook,audit.gatekeeper.sh,gator.gatekeeper.shshift-left,vap.k8s.io).status.totalViolationsandstatus.violations[](capped at 20 by--constraint-violations-limit) are filled by the audit loop every--audit-interval(60s). - Mutation (
mutations.gatekeeper.sh/v1):Assign(any field under spec,location: "spec.containers[name: *].imagePullPolicy",parameters.assign.value, optionalpathTestswithMustExist/MustNotExist),AssignMetadata(labels and annotations only, can copyfromMetadatanamespace or name),ModifySet(add to or prune a list),AssignImage(assignDomain,assignPath,assignTag). All but AssignMetadata needapplyTowith explicit groups/kinds/versions (no globs). Mutation runs only on CREATE and UPDATE and only if the mutating webhook was enabled at install (--operation=mutation-webhook). - Referential and expansion. Rego that needs other objects (
data.inventory) requires those kinds synced with aSyncSet(orConfig.spec.sync.syncOnly). AnExpansionTemplatemakes pod policies apply to Deployments by expandingspec.templateinto a pretend Pod at admission. External data: aProvider(externaldata.gatekeeper.sh) points at an in-cluster service, and templates callexternal_data({"provider": ..., "keys": [...]}); Ratify and the cosign provider are the signature-verification uses. - Operations. The webhook is installed with
failurePolicy: Ignoreby default (fail open); switching toFailis a deliberate step and the docs pair it with atimeoutSecondsof a few seconds and theadmission.gatekeeper.sh/ignorenamespace label for exemptions.gator test -f manifests/ -f policies/evaluates objects offline and exits 1 on violations;gator verifyruns aSuiteof test cases. Warn output appears in kubectl asWarning: [constraint-name] message.
Kubernetes native policies
| Field | ValidatingAdmissionPolicy (GA 1.30, admissionregistration.k8s.io/v1) | MutatingAdmissionPolicy (GA 1.36) |
|---|---|---|
| matchConstraints | resourceRules[] (groups, versions, operations, resources), namespaceSelector, objectSelector, matchPolicy | |
| matchConditions | CEL pre-filters; all must be true or the policy is skipped (a failing one obeys failurePolicy) | |
| variables | named CEL expressions reusable as variables.x | |
| validations[] / mutations[] | expression, message, messageExpression, reason | patchType: ApplyConfiguration (Object{...} partial) or JSONPatch (array of ops); reinvocationPolicy: Never|IfNeeded |
| auditAnnotations | key + valueExpression, written into the audit event | none |
| paramKind + binding paramRef | parameters from any object (a ConfigMap, a CRD); paramRef.parameterNotFoundAction: Allow|Deny is required | |
| binding.validationActions | [Deny], [Warn], [Audit] or [Warn, Audit]; Deny and Warn together are rejected | n/a |
| CEL variables | object (null on DELETE), oldObject (null on CREATE), request, params, namespaceObject, authorizer | |
| failurePolicy | Fail (default) or Ignore, on the policy, covering errors and cost-budget exhaustion | |
A denial reads ValidatingAdmissionPolicy 'x' with binding 'y' denied request: <message>. Type errors are surfaced in the policy's status.typeChecking rather than at apply time, so a policy with a typo in a field path is accepted and then either denies everything or nothing, depending on failurePolicy. Read the status after creating one.
Webhook mechanics every engine shares
In a ValidatingWebhookConfiguration or MutatingWebhookConfiguration: rules[] (operations, groups, versions, resources, scope), clientConfig (a Service plus caBundle, or a URL), failurePolicy (Fail is the API default; Gatekeeper installs Ignore, Kyverno Fail), timeoutSeconds (default 10, 1-30), sideEffects (None or NoneOnDryRun for the webhook to be called on --dry-run=server), matchConditions (CEL, since 1.30 GA), namespaceSelector/objectSelector, reinvocationPolicy: IfNeeded on mutating webhooks so a later mutation is re-checked by earlier ones, and matchPolicy: Equivalent so a rule on apps/v1 also catches apps/v1beta1 requests. Mutating webhooks run before validating ones, in an order you do not control; validating ones see the mutated object.
Which engine
| Need | Pick | Because |
|---|---|---|
| a handful of invariants, no extra component allowed | ValidatingAdmissionPolicy (+ MutatingAdmissionPolicy for defaults) | in-process, no webhook to keep alive, GA on 1.36 |
| generate resources (default NetworkPolicy, quota, RoleBinding per namespace) | Kyverno GeneratingPolicy | the only engine of the three that creates objects and keeps them in sync |
| image signature or attestation checks | Kyverno ImageValidatingPolicy (built in) or Gatekeeper + external data provider (Ratify) | needs registry access; native policies cannot fetch anything |
| a library of reusable, parameterized policies with typed parameters and an audit of existing objects | Gatekeeper ConstraintTemplate + Constraints | the template/instance split and gatekeeper-library; audit violations on the Constraint status |
| policy that reads other cluster objects | Kyverno (context, apiCall, GlobalContextEntry) or Gatekeeper with SyncSet | VAP can read only params and namespaceObject |
| report-then-enforce rollout with per-workload exceptions | Kyverno (Audit, PolicyReport, PolicyException) or Gatekeeper (dryrun/warn, audit) | VAP's Audit action only writes audit annotations; no report objects |
| the same rule enforced in CI before the cluster | Kyverno CLI (kyverno test, JSON mode) or gator test | both evaluate manifests offline; VAP has no CLI |
Kyverno and Gatekeeper appear in separate tasks, so the choice is usually made for you; the skill is writing the right fields in the dialect named. In Kyverno, check validationActions/failureAction before anything else, because samples default to audit. In Gatekeeper, check enforcementAction and that the Constraint's kinds match the object you are testing. For both, prove the denial with the apply error text and the report or violation count, and remember the tenant-namespace LimitRange trap from the exercises.
Operating policy safely
failurePolicy.Failmeans an unreachable webhook blocks the write: secure, and capable of freezing the entire cluster if the policy pods die.Ignoremeans writes proceed unchecked: available, and a bypass window during an outage. There is no universally correct answer, only a deliberate choice. Bound the risk withtimeoutSecondsand a well-scopednamespaceSelector.- Scope your webhooks.
rules(which resources and operations),namespaceSelectorandobjectSelectordecide what is intercepted. Exemptingkube-systemis near-universal, and it is itself a governance decision: it means a cluster-admin path exists that your policies do not see. - Latency. Every intercepted write pays a round trip. Broad wildcard rules on
*/*are how a policy engine becomes a cluster-wide performance problem. - Order. Mutating webhooks run in an order you do not fully control and may run more than once (re-invocation policy), so mutations must be idempotent.
- Where the message goes. A denial arrives in the apply error, verbatim. Write policy messages a stranger can act on: what is wrong, and what to change.
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
kubectl get policyreports -A # kyverno findings, governance-as-data
kubectl get constraints # gatekeeper, with violation counts
kubectl -n kyverno logs deploy/kyverno-admission-controller | tail -50outputcaptured 2026-08-26
$ kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
NAME WEBHOOKS AGE
validatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-validating-webhook-configuration 5 52m
validatingwebhookconfiguration.admissionregistration.k8s.io/config.webhook.pipeline.tekton.dev 1 55m
validatingwebhookconfiguration.admissionregistration.k8s.io/config.webhook.triggers.tekton.dev 1 55m
validatingwebhookconfiguration.admissionregistration.k8s.io/crossplane-no-usages 1 53m
validatingwebhookconfiguration.admissionregistration.k8s.io/externalsecret-validate 1 39m
validatingwebhookconfiguration.admissionregistration.k8s.io/gatekeeper-validating-webhook-configuration 2 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-cel-exception-validating-webhook-cfg 1 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-cleanup-validating-webhook-cfg 1 42m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-exception-validating-webhook-cfg 1 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-global-context-validating-webhook-cfg 1 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-policy-validating-webhook-cfg 1 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-resource-validating-webhook-cfg 1 41m
validatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-ttl-validating-webhook-cfg 1 42m
validatingwebhookconfiguration.admissionregistration.k8s.io/opentelemetry-operator-validation 4 46m
validatingwebhookconfiguration.admissionregistration.k8s.io/prometheus-kube-prometheus-admission 2 50m
validatingwebhookconfiguration.admissionregistration.k8s.io/secretstore-validate 2 39m
validatingwebhookconfiguration.admissionregistration.k8s.io/spire-spire-controller-manager-webhook 2 37m
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.pipeline.tekton.dev 1 55m
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.triggers.tekton.dev 1 55m
NAME WEBHOOKS AGE
mutatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-mutating-webhook-configuration 4 52m
mutatingwebhookconfiguration.admissionregistration.k8s.io/gatekeeper-mutating-webhook-configuration 1 41m
mutatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-policy-mutating-webhook-cfg 1 41m
mutatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-resource-mutating-webhook-cfg 1 41m
mutatingwebhookconfiguration.admissionregistration.k8s.io/kyverno-verify-mutating-webhook-cfg 1 41m
mutatingwebhookconfiguration.admissionregistration.k8s.io/opentelemetry-operator-mutation 3 46m
mutatingwebhookconfiguration.admissionregistration.k8s.io/prometheus-kube-prometheus-admission 1 50m
mutatingwebhookconfiguration.admissionregistration.k8s.io/vpa-webhook-config 1 59m
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.pipeline.tekton.dev 1 55m
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.triggers.tekton.dev 1 55m
$ kubectl get policyreports -A # kyverno findings, governance-as-data
NAMESPACE NAME KIND NAME PASS FAIL WARN ERROR SKIP AGE
argo-rollouts 1086be52-6ae4-473e-adc9-539fecd83956 Pod argo-rollouts-dashboard-768976b9f-pc4p7 0 1 0 0 0 37m
argo-rollouts 2b7dcfb8-31f3-4310-bc83-2cdf44fe01da Deployment argo-rollouts-dashboard 0 2 0 0 0 37m
argo-rollouts 62bcfbc0-50de-47d1-8d4c-03cea8a7ada1 Pod argo-rollouts-6c7457465f-ntvlt 0 1 0 0 0 37m
argo-rollouts 67550dfb-4b01-4853-8444-129e737f9de8 Pod argo-rollouts-6c7457465f-mdx8c 0 1 0 0 0 37m
argo-rollouts 73977456-456f-498a-83b2-212c09d728b5 ReplicaSet argo-rollouts-dashboard-768976b9f 0 1 0 0 0 36m
argo-rollouts b1a56daf-7c4f-4e69-9006-6f4179ffe6a9 Deployment argo-rollouts 0 2 0 0 0 37m
argo-rollouts b4f220d3-9df7-4917-930d-8dc71c5340a3 ReplicaSet argo-rollouts-6c7457465f 0 1 0 0 0 36m
argo 78dd4a7d-eae4-457e-9225-2944d01b88cd Deployment argo-workflows-server 0 2 0 0 0 37m
argo 934e7f1b-532a-4feb-b8dd-455a293461f5 Pod argo-workflows-server-68f86c79dc-s9xbn 0 1 0 0 0 37m
argo b8bb7d41-6271-4f35-8a11-7ab70a5c9f0f Deployment argo-workflows-workflow-controller 0 2 0 0 0 37m
argo e6b28c00-42d8-4682-b002-104ccee47f45 Pod argo-workflows-workflow-controller-58b6c468bd-kgcpk 0 1 0 0 0 37m
argo f136c264-4b50-4677-a421-5e0efc391872 ReplicaSet argo-workflows-server-68f86c79dc 0 1 0 0 0 36m
argo fa9fb582-c591-4921-bb9d-6ff429152cbd ReplicaSet argo-workflows-workflow-controller-58b6c468bd 0 1 0 0 0 36m
argocd 003a3a4b-490a-4c3f-b0d9-dc75f07e3f64 Pod argocd-server-5dfccb7d6b-gnwnl 0 1 0 0 0 37m
argocd 4688cafe-d911-4b2b-8bb9-c3748d513b4e Deployment argocd-server 0 2 0 0 0 37m
argocd 4f081113-6567-4441-b698-326bd9b0ef60 ReplicaSet argocd-applicationset-controller-54db99f499 0 1 0 0 0 36m
argocd 6060093b-3490-4e57-a3dd-a313b25daddf Pod argocd-application-controller-0 0 1 0 0 0 37m
argocd 70b2a469-5782-4484-b1b2-91d3cc99c945 ReplicaSet argocd-redis-546d94bfb4 0 1 0 0 0 36m
argocd 8302c045-638c-4b05-8797-1bb9639e7a67 Deployment argocd-repo-server 0 2 0 0 0 37m
$ kubectl get constraints # gatekeeper, with violation counts
error: the server doesn't have a resource type "constraints"
$ kubectl -n kyverno logs deploy/kyverno-admission-controller | tail -50
Defaulted container "kyverno" out of: kyverno, kyverno-pre (init)
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:305 > Starting workers controller=mutatingpolicy controllerGroup=policies.kyverno.io controllerKind=MutatingPolicy v=0 worker count=1
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:302 > Starting Controller controller=namespacedvalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedValidatingPolicy v=0
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:302 > Starting Controller controller=imagevalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=ImageValidatingPolicy v=0
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:302 > Starting Controller controller=namespacedmutatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedMutatingPolicy v=0
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:305 > Starting workers controller=namespacedvalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedValidatingPolicy v=0 worker count=1
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:305 > Starting workers controller=namespacedmutatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedMutatingPolicy v=0 worker count=1
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:305 > Starting workers controller=imagevalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=ImageValidatingPolicy v=0 worker count=1
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:302 > Starting Controller controller=namespacedimagevalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedImageValidatingPolicy v=0
2026-08-27T02:02:23Z INF sigs.k8s.io/controller-runtime@v0.24.1/pkg/internal/controller/controller.go:305 > Starting workers controller=namespacedimagevalidatingpolicy controllerGroup=policies.kyverno.io controllerKind=NamespacedImageValidatingPolicy v=0 worker count=1
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1beta1.PolicyException v=2
2026-08-27T02:02:23Z INF k8s.io/client-go@v0.36.3/tools/leaderelection/leaderelection.go:258 > Attempting to acquire leader lease... lock=kyverno/kyverno logger=klog v=0
2026-08-27T02:02:23Z TRC github.com/kyverno/kyverno/cmd/internal/controller.go:32 > starting controller logger=setup name=kyverno-events v=2 workers=3
2026-08-27T02:02:23Z TRC github.com/kyverno/kyverno/cmd/internal/controller.go:32 > starting controller logger=setup name=global-context v=2 workers=1
2026-08-27T02:02:23Z TRC github.com/kyverno/kyverno/pkg/event/controller.go:125 > start logger=EventGenerator v=2
2026-08-27T02:02:23Z TRC github.com/kyverno/kyverno/cmd/internal/controller.go:32 > starting controller logger=setup/controllers name=policycache-controller v=2 workers=3
2026-08-27T02:02:23Z INF k8s.io/client-go@v0.36.3/tools/leaderelection/leaderelection.go:272 > Successfully acquired lease lock=kyverno/kyverno logger=klog v=0
2026-08-27T02:02:23Z TRC github.com/kyverno/kyverno/pkg/leaderelection/leaderelection.go:82 > started leading id=kyverno-admission-controller-5b78bf8c6b-sxsh4 logger=setup/leader-election v=2
2026-08-27T02:02:23Z INF github.com/kyverno/kyverno/cmd/kyverno/main.go:320 > Initializing MutatingAdmissionPolicy informers for v1 v=0
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1beta1.NamespacedValidatingPolicy v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1.Lease v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1.MutatingAdmissionPolicyBinding v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1beta1.ImageValidatingPolicy v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v2.PolicyException v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1.ValidatingAdmissionPolicyBinding v=2
2026-08-27T02:02:23Z TRC k8s.io/client-go@v0.36.3/tools/cache/reflector.go:507 > Caches populated logger=klog reflector=k8s.io/client-go@v0.36.3/tools/cache/reflector.go:343 type=*v1.MutatingAdmissionPolicy v=2
… (rest of the tail omitted)A cluster that rejects everything mysteriously usually has a policy engine's webhook with a dead backend; the first command finds it, and failurePolicy explains it.
Exercises
One trap to dodge first: the tenant namespaces cannot demonstrate this policy at all. Their LimitRanges inject requests, and LimitRanger is a built-in mutating admission plugin that runs before validating webhooks. By the time Kyverno sees a team-a or team-b pod, it already has requests. So drill in a namespace with no LimitRange:
kubectl create ns policy-test
kubectl -n policy-test run naked --image=busybox:1.37 --restart=Never -- sleep 60 # succeeds: Audit
kubectl get policyreports -n policy-test # the violation is recorded instead
kubectl patch validatingpolicy require-resource-requests --type=merge \
-p '{"spec":{"validationActions":["Deny"]}}'
kubectl -n policy-test run naked2 --image=busybox:1.37 --restart=Never -- sleep 60outputcaptured 2026-08-26
$ kubectl create ns policy-test
namespace/policy-test created
$ kubectl -n policy-test run naked --image=busybox:1.37 --restart=Never -- sleep 60 # succeeds: Audit
pod/naked created
$ kubectl get policyreports -n policy-test # the violation is recorded instead
NAME KIND NAME PASS FAIL WARN ERROR SKIP AGE
f60c73a9-33c8-4479-8708-7cbc10ad5c62 Pod naked 0 1 0 0 0 25s
$ kubectl patch validatingpolicy require-resource-requests --type=merge \
-p '{"spec":{"validationActions":["Deny"]}}'
validatingpolicy.policies.kyverno.io/require-resource-requests patched
$ kubectl -n policy-test run naked2 --image=busybox:1.37 --restart=Never -- sleep 60
Error from server: admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy require-resource-requests failed: every container must set cpu and memory requests["Audit"] when done.Create a Deployment in team-b, then read it back: kubectl -n team-b get deploy <name> -o jsonpath='{.metadata.labels.cost-centre}' prints the namespace name, put there at admission by add-cost-centre-label. Verify against a deployment created before the policy existed (none of the lab's have the label).
Same rule, Rego dialect, same LimitRange-free namespace. ConstraintTemplate k8srequireresources whose Rego denies containers missing resource requests, then a constraint targeting Pods in policy-test with enforcementAction: warn first. Gatekeeper's library (open-policy-agent/gatekeeper-library) has a containerrequests template to adapt; adapting library Rego rather than writing from scratch is the honest workflow.
kubectl get k8srequireresources -o yaml shows audit violations counted. You can now articulate the trade: CEL policies read like schemas, Rego like code; Kyverno mutates and generates, Gatekeeper's audit and library are mature.Express the same rule as a ValidatingAdmissionPolicy + binding (CEL: object.spec.containers.all(c, has(c.resources.requests)), match Pods, bound to policy-test).
policy-test namespace, so the engines' results stay interpretable.kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations and, for one Kyverno entry, read rules (which resources), namespaceSelector (what is exempt) and failurePolicy.
kube-system in the selector while you are there; policy engines exempting the control plane is itself a governance decision.)Kyverno has two policy APIs at once: the classic ClusterPolicy and the CEL-based ValidatingPolicy. Which one a cluster prefers changes the answer to every policy task, and the API server tells you.
kubectl api-resources | grep -E 'kyverno'
kubectl -n kyverno get deploy -o jsonpath='{range .items[*]}{.metadata.name} {.spec.template.spec.containers[0].image}{"\n"}{end}'
kubectl apply -f - <<'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: legacy-shape }
spec:
validationFailureAction: Audit
rules:
- name: has-team-label
match:
any:
- resources: { kinds: [Pod], namespaces: [default] }
validate:
message: "pods should carry a team label"
pattern:
metadata:
labels:
team: "?*"
EOF
kubectl delete clusterpolicy legacy-shapeoutputcaptured 2026-09-12
$ kubectl api-resources | grep -E 'kyverno'
cleanuppolicies cleanpol kyverno.io/v2 true CleanupPolicy
clustercleanuppolicies ccleanpol kyverno.io/v2 false ClusterCleanupPolicy
clusterpolicies cpol kyverno.io/v1 false ClusterPolicy
globalcontextentries gctxentry kyverno.io/v2 false GlobalContextEntry
policies pol kyverno.io/v1 true Policy
policyexceptions polex kyverno.io/v2 true PolicyException
updaterequests ur kyverno.io/v2 true UpdateRequest
deletingpolicies dpol policies.kyverno.io/v1 false DeletingPolicy
generatingpolicies gpol policies.kyverno.io/v1 false GeneratingPolicy
imagevalidatingpolicies ivpol policies.kyverno.io/v1 false ImageValidatingPolicy
mutatingpolicies mpol policies.kyverno.io/v1 false MutatingPolicy
namespaceddeletingpolicies ndpol policies.kyverno.io/v1 true NamespacedDeletingPolicy
namespacedgeneratingpolicies ngpol policies.kyverno.io/v1 true NamespacedGeneratingPolicy
namespacedimagevalidatingpolicies nivpol policies.kyverno.io/v1 true NamespacedImageValidatingPolicy
namespacedmutatingpolicies nmpol policies.kyverno.io/v1 true NamespacedMutatingPolicy
namespacedvalidatingpolicies nvpol policies.kyverno.io/v1 true NamespacedValidatingPolicy
policyexceptions policies.kyverno.io/v1 true PolicyException
validatingpolicies vpol policies.kyverno.io/v1 false ValidatingPolicy
clusterephemeralreports cephr reports.kyverno.io/v1 false ClusterEphemeralReport
ephemeralreports ephr reports.kyverno.io/v1 true EphemeralReport
$ kubectl -n kyverno get deploy -o jsonpath='{range .items[*]}{.metadata.name} {.spec.template.spec.containers[0].image}{"\n"}{end}'
kyverno-admission-controller reg.kyverno.io/kyverno/kyverno:v1.19.1
kyverno-background-controller reg.kyverno.io/kyverno/background-controller:v1.19.1
kyverno-cleanup-controller reg.kyverno.io/kyverno/cleanup-controller:v1.19.1
kyverno-reports-controller reg.kyverno.io/kyverno/reports-controller:v1.19.1
$ kubectl apply -f - <<'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: legacy-shape }
spec:
validationFailureAction: Audit
rules:
- name: has-team-label
match:
any:
- resources: { kinds: [Pod], namespaces: [default] }
validate:
message: "pods should carry a team label"
pattern:
metadata:
labels:
team: "?*"
EOF
Warning: kyverno.io/v1 ClusterPolicy is deprecated and will be removed in a future release; migrate to ValidatingPolicy, MutatingPolicy, GeneratingPolicy or ImageValidatingPolicy (policies.kyverno.io), see https://kyverno.io/docs/guides/migration-to-cel/
clusterpolicy.kyverno.io/legacy-shape created
$ kubectl delete clusterpolicy legacy-shape
Warning: kyverno.io/v1 ClusterPolicy is deprecated and will be removed in a future release; migrate to ValidatingPolicy, MutatingPolicy, GeneratingPolicy or ImageValidatingPolicy (policies.kyverno.io), see https://kyverno.io/docs/guides/migration-to-cel/
clusterpolicy.kyverno.io "legacy-shape" deletedA pod-level policy catches the pod, which is created by a ReplicaSet, which means the developer's kubectl apply succeeds and the failure lands in an event nobody reads. Autogen moves the denial to where the human is.
kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata: { name: require-team-label }
spec:
# autogen is on by default, so without this the first half is already a Deployment-level deny
autogen:
podControllers:
controllers: ["none"]
validationActions: [Deny]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels) && has(object.metadata.labels.team)"
message: "every pod must carry a team label"
EOF
sleep 15
kubectl -n default create deployment unlabelled --image=nginx:1.27-alpine
sleep 20
kubectl -n default get deploy unlabelled -o jsonpath='{.status.conditions}' | jq '.[] | {type, status, reason, message}'
kubectl -n default get events --field-selector reason=FailedCreate | tail -3
kubectl -n default delete deployment unlabelled
kubectl patch validatingpolicy require-team-label --type merge -p '{"spec":{"autogen":{"podControllers":{"controllers":["deployments"]}}}}'
sleep 20
kubectl -n default create deployment unlabelled --image=nginx:1.27-alpine
kubectl -n default delete deployment unlabelled --ignore-not-found
kubectl delete validatingpolicy require-team-label
kubectl get validatingpolicy require-team-label -o jsonpath='{.spec.autogen}{"\n"}'outputcaptured 2026-09-13
$ kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata: { name: require-team-label }
spec:
# autogen is on by default, so without this the first half is already a Deployment-level deny
autogen:
podControllers:
controllers: ["none"]
validationActions: [Deny]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels) && has(object.metadata.labels.team)"
message: "every pod must carry a team label"
EOF
validatingpolicy.policies.kyverno.io/require-team-label created
$ sleep 15
$ kubectl -n default create deployment unlabelled --image=nginx:1.27-alpine
deployment.apps/unlabelled created
$ sleep 20
$ kubectl -n default get deploy unlabelled -o jsonpath='{.status.conditions}' | jq '.[] | {type, status, reason, message}'
{
"type": "Progressing",
"status": "True",
"reason": "NewReplicaSetCreated",
"message": "Created new replica set \"unlabelled-6896b8687b\""
}
{
"type": "Available",
"status": "False",
"reason": "MinimumReplicasUnavailable",
"message": "Deployment does not have minimum availability."
}
{
"type": "ReplicaFailure",
"status": "True",
"reason": "FailedCreate",
"message": "admission webhook \"vpol.validate.kyverno.svc-fail\" denied the request: Policy require-team-label failed: every pod must carry a team label"
}
$ kubectl -n default get events --field-selector reason=FailedCreate | tail -3
LAST SEEN TYPE REASON OBJECT MESSAGE
10s Warning FailedCreate replicaset/unlabelled-6896b8687b Error creating: admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy require-team-label failed: every pod must carry a team label
$ kubectl -n default delete deployment unlabelled
deployment.apps "unlabelled" deleted from default namespace
$ kubectl patch validatingpolicy require-team-label --type merge -p '{"spec":{"autogen":{"podControllers":{"controllers":["deployments"]}}}}'
validatingpolicy.policies.kyverno.io/require-team-label patched
$ sleep 20
$ kubectl -n default create deployment unlabelled --image=nginx:1.27-alpine
error: failed to create deployment: admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy require-team-label failed: every pod must carry a team label
$ kubectl -n default delete deployment unlabelled --ignore-not-found
$ kubectl delete validatingpolicy require-team-label
validatingpolicy.policies.kyverno.io "require-team-label" deleted
$ kubectl get validatingpolicy require-team-label -o jsonpath='{.spec.autogen}{"\n"}'
Error from server (NotFound): validatingpolicies.policies.kyverno.io "require-team-label" not foundkubectl create itself is refused.Kyverno can compile a ValidatingPolicy down to a native ValidatingAdmissionPolicy, which then runs in the API server with no webhook at all. Read the generated object; it is the best available lesson in what CEL admission looks like.
kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata: { name: no-latest }
spec:
validationActions: [Deny]
autogen:
validatingAdmissionPolicy: { enabled: true }
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "no :latest tags"
EOF
sleep 60
kubectl get validatingadmissionpolicies
kubectl get validatingadmissionpolicybindings
kubectl get validatingadmissionpolicy -o jsonpath='{range .items[*]}{.metadata.name} {.spec.validations[*].expression}{"\n"}{end}'
kubectl get validatingpolicy no-latest -o jsonpath='{.spec.autogen}{"\n"}'
kubectl -n kyverno logs deploy/kyverno-admission-controller --tail=50 | grep -i -m5 'validatingadmissionpolicy' || true
kubectl delete validatingpolicy no-latestoutputcaptured 2026-09-13
$ kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata: { name: no-latest }
spec:
validationActions: [Deny]
autogen:
validatingAdmissionPolicy: { enabled: true }
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.spec.containers.all(c, !c.image.endsWith(':latest'))"
message: "no :latest tags"
EOF
validatingpolicy.policies.kyverno.io/no-latest created
$ sleep 60
$ kubectl get validatingadmissionpolicies
NAME VALIDATIONS PARAMKIND AGE
safe-upgrades.gateway.networking.k8s.io 2 <unset> 20h
vpol-no-latest 1 <unset> 60s
$ kubectl get validatingadmissionpolicybindings
NAME POLICYNAME PARAMREF AGE
safe-upgrades.gateway.networking.k8s.io safe-upgrades.gateway.networking.k8s.io <unset> 20h
vpol-no-latest-binding vpol-no-latest <unset> 60s
$ kubectl get validatingadmissionpolicy -o jsonpath='{range .items[*]}{.metadata.name} {.spec.validations[*].expression}{"\n"}{end}'
safe-upgrades.gateway.networking.k8s.io object.spec.group != 'gateway.networking.k8s.io' || oldObject == null || ( has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && object.metadata.annotations['gateway.networking.k8s.io/channel'] == 'standard' ) || ( oldObject != null && has(oldObject.metadata.annotations) && oldObject.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && oldObject.metadata.annotations['gateway.networking.k8s.io/channel'] == 'experimental' ) object.spec.group != 'gateway.networking.k8s.io' || (has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/bundle-version') && ( matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], '-(rc)') || ( !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v1.[0-5].\\d+') && !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v0') ) ))
vpol-no-latest object.spec.containers.all(c, !c.image.endsWith(':latest'))
$ kubectl get validatingpolicy no-latest -o jsonpath='{.spec.autogen}{"\n"}'
{"validatingAdmissionPolicy":{"enabled":true}}
$ kubectl -n kyverno logs deploy/kyverno-admission-controller --tail=50 | grep -i -m5 'validatingadmissionpolicy' || true
Defaulted container "kyverno" out of: kyverno, kyverno-pre (init)
$ kubectl delete validatingpolicy no-latest
validatingpolicy.policies.kyverno.io "no-latest" deletedvalidatingAdmissionPolicy autogen block you asked for, and a ValidatingAdmissionPolicy and binding named no-latest appear alongside the pre-existing safe-upgrades.gateway.networking.k8s.io, carrying your CEL expression. If the autogen block reads back empty, the field was pruned by the API version you applied and nothing will ever be generated. If it is present and no policy appears, the admission controller log names the reason."Just this once" has to be an object, not a conversation. A PolicyException names the policy and the resources it excuses, and the report records a skip rather than a pass, which keeps the exception visible.
kubectl -n kyverno get deploy kyverno-admission-controller -o jsonpath='{.spec.template.spec.containers[0].args}' | jq
kubectl create ns policy-exception --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata: { name: legacy-job, namespace: policy-exception }
spec:
policyRefs:
- name: require-resource-requests
kind: ValidatingPolicy
matchConditions:
- name: only-the-legacy-pod
expression: "object.metadata.name == 'legacy'"
EOF
kubectl -n default run legacy --image=nginx:1.27-alpine --restart=Never
# the reports controller runs behind admission, so wait for the report rather than guessing a sleep
for i in $(seq 18); do kubectl -n default get policyreport -o json | jq -e '[.items[]|select(.scope.name=="legacy")]|length>0' >/dev/null && break; sleep 5; done; echo "report after ~$((i*5))s"
kubectl -n default get policyreport
kubectl -n default get policyreport -o json | jq -r '.items[] | select(.scope.name=="legacy") | .results[] | "\(.policy) \(.result)"'
kubectl -n default delete pod legacy --ignore-not-found
kubectl -n policy-exception delete policyexceptions.policies.kyverno.io legacy-job
kubectl delete ns policy-exceptionoutputcaptured 2026-09-13
$ kubectl -n kyverno get deploy kyverno-admission-controller -o jsonpath='{.spec.template.spec.containers[0].args}' | jq
[
"--caSecretName=kyverno-svc.kyverno.svc.kyverno-tls-ca",
"--tlsSecretName=kyverno-svc.kyverno.svc.kyverno-tls-pair",
"--tlsKeyAlgorithm=RSA",
"--backgroundServiceAccountName=system:serviceaccount:kyverno:kyverno-background-controller",
"--reportsServiceAccountName=system:serviceaccount:kyverno:kyverno-reports-controller",
"--servicePort=443",
"--webhookServerPort=9443",
"--resyncPeriod=15m",
"--crdWatcher=false",
"--disableMetrics=false",
"--otelConfig=prometheus",
"--metricsPort=8000",
"--admissionReports=true",
"--maxAdmissionReports=1000",
"--autoUpdateWebhooks=true",
"--excludeBootstrapResources=false",
"--enableConfigMapCaching=true",
"--controllerRuntimeMetricsAddress=:8080",
"--enableDeferredLoading=true",
"--dumpPayload=false",
"--forceFailurePolicyIgnore=false",
"--generateValidatingAdmissionPolicy=true",
"--generateMutatingAdmissionPolicy=false",
"--dumpPatches=false",
"--maxAPICallResponseLength=2000000",
"--apiCallTimeout=30s",
"--maxGlobalContextEntries=0",
"--loggingFormat=text",
"--v=2",
"--omitEvents=PolicyApplied,PolicySkipped",
"--enablePolicyException=true",
"--protectManagedResources=false",
"--allowInsecureRegistry=true",
"--registryCredentialHelpers=default,google,amazon,azure,github",
"--enableReporting=validate,mutate,mutateExisting,imageVerify,generate"
]
$ kubectl create ns policy-exception --dry-run=client -o yaml | kubectl apply -f -
namespace/policy-exception created
$ kubectl apply -f - <<'EOF'
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata: { name: legacy-job, namespace: policy-exception }
spec:
policyRefs:
- name: require-resource-requests
kind: ValidatingPolicy
matchConditions:
- name: only-the-legacy-pod
expression: "object.metadata.name == 'legacy'"
EOF
policyexception.policies.kyverno.io/legacy-job created
$ kubectl -n default run legacy --image=nginx:1.27-alpine --restart=Never
pod/legacy created
$ # the reports controller runs behind admission, so wait for the report rather than guessing a sleep
$ for i in $(seq 18); do kubectl -n default get policyreport -o json | jq -e '[.items[]|select(.scope.name=="legacy")]|length>0' >/dev/null && break; sleep 5; done; echo "report after ~$((i*5))s"
report after ~25s
$ kubectl -n default get policyreport
NAME KIND NAME PASS FAIL WARN ERROR SKIP AGE
7a647270-939c-44f6-a2cf-126b21a8210d Pod legacy 0 0 0 0 1 1s
$ kubectl -n default get policyreport -o json | jq -r '.items[] | select(.scope.name=="legacy") | .results[] | "\(.policy) \(.result)"'
require-resource-requests skip
$ kubectl -n default delete pod legacy --ignore-not-found
pod "legacy" deleted from default namespace
$ kubectl -n policy-exception delete policyexceptions.policies.kyverno.io legacy-job
policyexception.policies.kyverno.io "legacy-job" deleted from policy-exception namespace
$ kubectl delete ns policy-exception
namespace "policy-exception" deletedrequire-resource-requests skip: the exception is on the record rather than making the resource disappear from the report. The flags are the other half of the answer, since --enablePolicyException=true is what makes an exception legal at all.The Kyverno CLI runs policies against manifests in CI, which is where a policy should fail first. A test file asserts the outcome per resource, so a policy change that breaks a team's manifests is caught before it merges.
START=$PWD # the cd below would otherwise follow you into every later command
mkdir -p "$HOME/.local/bin"; export PATH="$HOME/.local/bin:$PATH"
URL=$(curl -s https://api.github.com/repos/kyverno/kyverno/releases/latest | jq -r '.assets[] | select(.name | startswith("kyverno-cli_") and endswith("_linux_x86_64.tar.gz")) | .browser_download_url'); echo "$URL"
rm -rf /tmp/kyverno-cli && mkdir -p /tmp/kyverno-cli && curl -sL "$URL" | tar xz -C /tmp/kyverno-cli && install /tmp/kyverno-cli/kyverno "$HOME/.local/bin/"
kyverno version | head -2
rm -rf /tmp/kyverno-test && mkdir -p /tmp/kyverno-test && cd /tmp/kyverno-test
cp "$REPO_ROOT"/examples/kyverno/require-resources.yaml policy.yaml
cat > good-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: good, namespace: default }
spec:
containers:
- name: c
image: nginx:1.27-alpine
resources:
requests: { cpu: 25m, memory: 32Mi }
EOF
cat > bad-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: bad, namespace: default }
spec:
containers:
- name: c
image: nginx:1.27-alpine
EOF
kyverno apply policy.yaml --resource good-pod.yaml --resource bad-pod.yaml
cat > kyverno-test.yaml <<'EOF'
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata: { name: require-resources }
policies: [policy.yaml]
resources: [good-pod.yaml, bad-pod.yaml]
results:
- policy: require-resource-requests
resources: [good]
kind: Pod
result: pass
- policy: require-resource-requests
resources: [bad]
kind: Pod
result: fail
EOF
kyverno test .
cd "$START"outputcaptured 2026-09-13
$ START=$PWD # the cd below would otherwise follow you into every later command
$ mkdir -p "$HOME/.local/bin"; export PATH="$HOME/.local/bin:$PATH"
$ URL=$(curl -s https://api.github.com/repos/kyverno/kyverno/releases/latest | jq -r '.assets[] | select(.name | startswith("kyverno-cli_") and endswith("_linux_x86_64.tar.gz")) | .browser_download_url'); echo "$URL"
https://github.com/kyverno/kyverno/releases/download/v1.19.1/kyverno-cli_v1.19.1_linux_x86_64.tar.gz
$ rm -rf /tmp/kyverno-cli && mkdir -p /tmp/kyverno-cli && curl -sL "$URL" | tar xz -C /tmp/kyverno-cli && install /tmp/kyverno-cli/kyverno "$HOME/.local/bin/"
$ kyverno version | head -2
Version: 1.19.1
Time: 2026-09-10T05:20:21Z
$ rm -rf /tmp/kyverno-test && mkdir -p /tmp/kyverno-test && cd /tmp/kyverno-test
$ cp "$REPO_ROOT"/examples/kyverno/require-resources.yaml policy.yaml
$ cat > good-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: good, namespace: default }
spec:
containers:
- name: c
image: nginx:1.27-alpine
resources:
requests: { cpu: 25m, memory: 32Mi }
EOF
$ cat > bad-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: bad, namespace: default }
spec:
containers:
- name: c
image: nginx:1.27-alpine
EOF
$ kyverno apply policy.yaml --resource good-pod.yaml --resource bad-pod.yaml
Applying 1 policy rule(s) to 2 resource(s)...
policy require-resource-requests -> resource default/Pod/bad failed:
1 - every container must set cpu and memory requests
pass: 1, fail: 1, warn: 0, error: 0, skip: 0
$ cat > kyverno-test.yaml <<'EOF'
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata: { name: require-resources }
policies: [policy.yaml]
resources: [good-pod.yaml, bad-pod.yaml]
results:
- policy: require-resource-requests
resources: [good]
kind: Pod
result: pass
- policy: require-resource-requests
resources: [bad]
kind: Pod
result: fail
EOF
$ kyverno test .
Loading test ( kyverno-test.yaml ) ...
Loading values/variables ...
Loading policies ...
Loading resources ...
Loading exceptions ...
Applying 1 policy to 2 resources with 0 exceptions ...
Checking results ...
│────│───────────────────────────│──────│─────────────────────│────────│────────│
│ ID │ POLICY │ RULE │ RESOURCE │ RESULT │ REASON │
│────│───────────────────────────│──────│─────────────────────│────────│────────│
│ 1 │ require-resource-requests │ │ v1/Pod/default/good │ Pass │ Ok │
│ 2 │ require-resource-requests │ │ v1/Pod/default/bad │ Pass │ Ok │
│────│───────────────────────────│──────│─────────────────────│────────│────────│
Test Summary: 2 tests passed and 0 tests failed
$ cd "$START"kyverno apply passes one pod and fails the other, and kyverno test reports both assertions met.Gatekeeper's scoped enforcement lets one constraint behave differently at admission and in audit. Warning the person typing while recording a violation for the report is a realistic rollout strategy, not a compromise.
kubectl apply -f - <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: { name: k8srequiredlabels }
spec:
crd:
spec:
names: { kind: K8sRequiredLabels }
validation:
openAPIV3Schema:
type: object
properties:
labels: { type: array, items: { type: string } }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels[_]
not input.review.object.metadata.labels[required]
msg := sprintf("missing required label: %v", [required])
}
EOF
sleep 20
kubectl apply -f - <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata: { name: pods-need-owner }
spec:
enforcementAction: scoped
scopedEnforcementActions:
- action: warn
enforcementPoints: [{ name: validation.gatekeeper.sh }]
- action: deny
enforcementPoints: [{ name: audit.gatekeeper.sh }]
match:
kinds:
- { apiGroups: [""], kinds: [Pod] }
namespaces: [default]
parameters:
labels: [owner]
EOF
sleep 30
kubectl -n default run scoped-demo --image=nginx:1.27-alpine --restart=Never
sleep 90
kubectl get k8srequiredlabels pods-need-owner -o jsonpath='{.status.violations}' | jq
kubectl -n default delete pod scoped-demo --ignore-not-found
kubectl delete k8srequiredlabels pods-need-owner
kubectl delete constrainttemplate k8srequiredlabelsoutputcaptured 2026-09-12
$ kubectl apply -f - <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: { name: k8srequiredlabels }
spec:
crd:
spec:
names: { kind: K8sRequiredLabels }
validation:
openAPIV3Schema:
type: object
properties:
labels: { type: array, items: { type: string } }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels[_]
not input.review.object.metadata.labels[required]
msg := sprintf("missing required label: %v", [required])
}
EOF
constrainttemplate.templates.gatekeeper.sh/k8srequiredlabels created
$ sleep 20
$ kubectl apply -f - <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata: { name: pods-need-owner }
spec:
enforcementAction: scoped
scopedEnforcementActions:
- action: warn
enforcementPoints: [{ name: validation.gatekeeper.sh }]
- action: deny
enforcementPoints: [{ name: audit.gatekeeper.sh }]
match:
kinds:
- { apiGroups: [""], kinds: [Pod] }
namespaces: [default]
parameters:
labels: [owner]
EOF
k8srequiredlabels.constraints.gatekeeper.sh/pods-need-owner created
$ sleep 30
$ kubectl -n default run scoped-demo --image=nginx:1.27-alpine --restart=Never
Warning: [pods-need-owner] missing required label: owner
pod/scoped-demo created
$ sleep 90
$ kubectl get k8srequiredlabels pods-need-owner -o jsonpath='{.status.violations}' | jq
[
{
"enforcementAction": "scoped",
"enforcementActions": [
"deny"
],
"group": "",
"kind": "Pod",
"message": "missing required label: owner",
"name": "signed-86hrq-pod",
"namespace": "default",
"version": "v1"
},
{
"enforcementAction": "scoped",
"enforcementActions": [
"deny"
],
"group": "",
"kind": "Pod",
"message": "missing required label: owner",
"name": "scoped-demo",
"namespace": "default",
"version": "v1"
}
]
$ kubectl -n default delete pod scoped-demo --ignore-not-found
pod "scoped-demo" deleted from default namespace
$ kubectl delete k8srequiredlabels pods-need-owner
k8srequiredlabels.constraints.gatekeeper.sh "pods-need-owner" deleted
$ kubectl delete constrainttemplate k8srequiredlabels
constrainttemplate.templates.gatekeeper.sh "k8srequiredlabels" deletedGatekeeper mutates objects on the way in, and what keeps it from clobbering a value someone set deliberately is the kind you reach for: AssignMetadata fills a label in only when it is absent, where Assign needs a pathTests condition to hold it back. Mutation with neither is how a platform team breaks a team's deployment quietly.
# Assign refuses to touch metadata; labels and annotations are AssignMetadata's job,
# and it only ever fills in a value that is absent, so no pathTests condition is needed
kubectl apply -f - <<'EOF'
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata: { name: default-owner-label }
spec:
match:
scope: Namespaced
namespaces: [default]
kinds:
- { apiGroups: ["*"], kinds: [Pod] }
location: "metadata.labels.owner"
parameters:
assign:
value: platform
EOF
sleep 30
kubectl -n default run mutated --image=nginx:1.27-alpine --restart=Never
kubectl -n default get pod mutated -o jsonpath='{.metadata.labels.owner}{"\n"}'
kubectl -n default run untouched --image=nginx:1.27-alpine --restart=Never --labels=owner=team-a
kubectl -n default get pod untouched -o jsonpath='{.metadata.labels.owner}{"\n"}'
kubectl -n default delete pod mutated untouched --ignore-not-found
kubectl delete assignmetadata default-owner-labeloutputcaptured 2026-09-13
$ # Assign refuses to touch metadata; labels and annotations are AssignMetadata's job,
$ # and it only ever fills in a value that is absent, so no pathTests condition is needed
$ kubectl apply -f - <<'EOF'
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata: { name: default-owner-label }
spec:
match:
scope: Namespaced
namespaces: [default]
kinds:
- { apiGroups: ["*"], kinds: [Pod] }
location: "metadata.labels.owner"
parameters:
assign:
value: platform
EOF
assignmetadata.mutations.gatekeeper.sh/default-owner-label created
$ sleep 30
$ kubectl -n default run mutated --image=nginx:1.27-alpine --restart=Never
pod/mutated created
$ kubectl -n default get pod mutated -o jsonpath='{.metadata.labels.owner}{"\n"}'
platform
$ kubectl -n default run untouched --image=nginx:1.27-alpine --restart=Never --labels=owner=team-a
pod/untouched created
$ kubectl -n default get pod untouched -o jsonpath='{.metadata.labels.owner}{"\n"}'
team-a
$ kubectl -n default delete pod mutated untouched --ignore-not-found
pod "mutated" deleted from default namespace
pod "untouched" deleted from default namespace
$ kubectl delete assignmetadata default-owner-label
assignmetadata.mutations.gatekeeper.sh "default-owner-label" deletedowner set to platform, and the one that set owner=team-a keeps team-a. Note the kind: Assign refuses to touch metadata at all, so labels and annotations are AssignMetadata's job, and it only ever fills in a value that is absent. That "only if absent" is built into the kind rather than written as a condition, which is the difference between a default and a decree. imagePullPolicy is the wrong field for this drill either way: the API server defaults it before any mutating webhook runs, so a "mutate only when unset" rule on it can never fire.gator test evaluates Gatekeeper constraints against manifests on a laptop or a build agent. Same policies, same answers, no cluster, which is what makes policy-as-code reviewable.
START=$PWD # the cd below would otherwise follow you into every later command
mkdir -p "$HOME/.local/bin"; export PATH="$HOME/.local/bin:$PATH"
URL=$(curl -s https://api.github.com/repos/open-policy-agent/gatekeeper/releases/latest | jq -r '.assets[] | select(.name | startswith("gator-") and endswith("-linux-amd64.tar.gz")) | .browser_download_url'); echo "$URL"
rm -rf /tmp/gator-bin && mkdir -p /tmp/gator-bin && curl -sL "$URL" | tar xz -C /tmp/gator-bin && install /tmp/gator-bin/gator "$HOME/.local/bin/"
gator version
rm -rf /tmp/gator-ci && mkdir -p /tmp/gator-ci/policies /tmp/gator-ci/manifests && cd /tmp/gator-ci
cat > policies/template.yaml <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: { name: k8sdenylatest }
spec:
crd:
spec:
names: { kind: K8sDenyLatest }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdenylatest
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
endswith(c.image, ":latest")
msg := sprintf("image uses the latest tag: %v", [c.image])
}
EOF
cat > policies/constraint.yaml <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDenyLatest
metadata: { name: no-latest-anywhere }
spec:
match:
kinds:
- { apiGroups: [""], kinds: [Pod] }
EOF
cat > manifests/pods.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: pinned }
spec:
containers: [{ name: c, image: nginx:1.27-alpine }]
---
apiVersion: v1
kind: Pod
metadata: { name: floating }
spec:
containers: [{ name: c, image: nginx:latest }]
EOF
gator test -f manifests/ -f policies/; echo "exit code: $?"
cd "$START"outputcaptured 2026-09-12
$ START=$PWD # the cd below would otherwise follow you into every later command
$ mkdir -p "$HOME/.local/bin"; export PATH="$HOME/.local/bin:$PATH"
$ URL=$(curl -s https://api.github.com/repos/open-policy-agent/gatekeeper/releases/latest | jq -r '.assets[] | select(.name | startswith("gator-") and endswith("-linux-amd64.tar.gz")) | .browser_download_url'); echo "$URL"
https://github.com/open-policy-agent/gatekeeper/releases/download/v3.23.1/gator-v3.23.1-linux-amd64.tar.gz
$ rm -rf /tmp/gator-bin && mkdir -p /tmp/gator-bin && curl -sL "$URL" | tar xz -C /tmp/gator-bin && install /tmp/gator-bin/gator "$HOME/.local/bin/"
$ gator version
:::::::: ::: ::::::::::: :::::::: :::::::::
:+: :+: :+: :+: :+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
:#: +#++:++#++: +#+ +#+ +:+ +#++:++#:
+#+ +#+# +#+ +#+ +#+ +#+ +#+ +#+ +#+
#+# #+# #+# #+# #+# #+# #+# #+# #+#
######## ### ### ### ######## ### ###
gator: gator is a suite of authorship tools for Gatekeeper
GitVersion: v3.23.1+dirty
GitCommit: 2d0b6ad97cf3a8b8436a00a1f4b07b3a56b92ca0
GitTreeState: dirty
BuildDate: 2026-08-27T21:26:44
GoVersion: go1.26.0
Compiler: gc
Platform: linux/amd64
$ rm -rf /tmp/gator-ci && mkdir -p /tmp/gator-ci/policies /tmp/gator-ci/manifests && cd /tmp/gator-ci
$ cat > policies/template.yaml <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: { name: k8sdenylatest }
spec:
crd:
spec:
names: { kind: K8sDenyLatest }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdenylatest
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
endswith(c.image, ":latest")
msg := sprintf("image uses the latest tag: %v", [c.image])
}
EOF
$ cat > policies/constraint.yaml <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDenyLatest
metadata: { name: no-latest-anywhere }
spec:
match:
kinds:
- { apiGroups: [""], kinds: [Pod] }
EOF
$ cat > manifests/pods.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: pinned }
spec:
containers: [{ name: c, image: nginx:1.27-alpine }]
---
apiVersion: v1
kind: Pod
metadata: { name: floating }
spec:
containers: [{ name: c, image: nginx:latest }]
EOF
$ gator test -f manifests/ -f policies/; echo "exit code: $?"
v1/Pod floating: ["no-latest-anywhere"] Message: "image uses the latest tag: nginx:latest"
exit code: 1
$ cd "$START"A VAP with a typo in a field path does not fail loudly; it fails at evaluation, and what happens then depends entirely on the failure policy. The API server type-checks the expression up front and puts the result in status.
kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: { name: typo-policy }
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicaCount <= 10"
message: "at most ten replicas"
EOF
sleep 10
kubectl get validatingadmissionpolicy typo-policy -o jsonpath='{.status.typeChecking}' | jq
kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: { name: typo-policy-binding }
spec:
policyName: typo-policy
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: default }
EOF
sleep 10
kubectl -n default create deployment typo-test --image=nginx:1.27-alpine
kubectl patch validatingadmissionpolicy typo-policy --type merge -p '{"spec":{"failurePolicy":"Ignore"}}'
sleep 10
kubectl -n default create deployment typo-test --image=nginx:1.27-alpine
kubectl -n default delete deployment typo-test --ignore-not-found
kubectl delete validatingadmissionpolicybinding typo-policy-binding
kubectl delete validatingadmissionpolicy typo-policyoutputcaptured 2026-09-12
$ kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: { name: typo-policy }
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicaCount <= 10"
message: "at most ten replicas"
EOF
validatingadmissionpolicy.admissionregistration.k8s.io/typo-policy created
$ sleep 10
$ kubectl get validatingadmissionpolicy typo-policy -o jsonpath='{.status.typeChecking}' | jq
{
"expressionWarnings": [
{
"fieldRef": "spec.validations[0].expression",
"warning": "apps/v1, Kind=Deployment: ERROR: <input>:1:12: undefined field 'replicaCount'\n | object.spec.replicaCount <= 10\n | ...........^\n"
}
]
}
$ kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: { name: typo-policy-binding }
spec:
policyName: typo-policy
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: default }
EOF
validatingadmissionpolicybinding.admissionregistration.k8s.io/typo-policy-binding created
$ sleep 10
$ kubectl -n default create deployment typo-test --image=nginx:1.27-alpine
error: failed to create deployment: deployments.apps "typo-test" is forbidden: ValidatingAdmissionPolicy 'typo-policy' with binding 'typo-policy-binding' denied request: expression 'object.spec.replicaCount <= 10' resulted in error: no such key: replicaCount
$ kubectl patch validatingadmissionpolicy typo-policy --type merge -p '{"spec":{"failurePolicy":"Ignore"}}'
validatingadmissionpolicy.admissionregistration.k8s.io/typo-policy patched
$ sleep 10
$ kubectl -n default create deployment typo-test --image=nginx:1.27-alpine
deployment.apps/typo-test created
$ kubectl -n default delete deployment typo-test --ignore-not-found
deployment.apps "typo-test" deleted from default namespace
$ kubectl delete validatingadmissionpolicybinding typo-policy-binding
validatingadmissionpolicybinding.admissionregistration.k8s.io "typo-policy-binding" deleted
$ kubectl delete validatingadmissionpolicy typo-policy
validatingadmissionpolicy.admissionregistration.k8s.io "typo-policy" deletedstatus.typeChecking names the field that does not exist, the create is refused under Fail, and the identical broken policy lets everything through under Ignore. Read the type-checking status before you trust a policy you just wrote.MutatingAdmissionPolicy does for defaults what VAP did for validation: CEL in the API server, no controller to keep alive. Add a label with an apply configuration and watch it land on a pod nobody patched.
kubectl api-resources | grep -i mutatingadmissionpolicy
kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata: { name: add-platform-label }
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
reinvocationPolicy: Never
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: |
Object{ metadata: Object.metadata{ labels: {"managed-by": "platform"} } }
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata: { name: add-platform-label-binding }
spec:
policyName: add-platform-label
matchResources:
namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: default }
EOF
sleep 15
kubectl -n default run mutated-native --image=nginx:1.27-alpine --restart=Never --overrides='{"spec":{"containers":[{"name":"mutated-native","image":"nginx:1.27-alpine","resources":{"requests":{"cpu":"25m","memory":"32Mi"}}}]}}'
kubectl -n default get pod mutated-native -o jsonpath='{.metadata.labels}{"\n"}'
kubectl -n default delete pod mutated-native --ignore-not-found
kubectl delete mutatingadmissionpolicybinding add-platform-label-binding
kubectl delete mutatingadmissionpolicy add-platform-labeloutputcaptured 2026-09-12
$ kubectl api-resources | grep -i mutatingadmissionpolicy
mutatingadmissionpolicies admissionregistration.k8s.io/v1 false MutatingAdmissionPolicy
mutatingadmissionpolicybindings admissionregistration.k8s.io/v1 false MutatingAdmissionPolicyBinding
$ kubectl apply -f - <<'EOF'
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata: { name: add-platform-label }
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
reinvocationPolicy: Never
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: |
Object{ metadata: Object.metadata{ labels: {"managed-by": "platform"} } }
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata: { name: add-platform-label-binding }
spec:
policyName: add-platform-label
matchResources:
namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: default }
EOF
mutatingadmissionpolicy.admissionregistration.k8s.io/add-platform-label created
mutatingadmissionpolicybinding.admissionregistration.k8s.io/add-platform-label-binding created
$ sleep 15
$ kubectl -n default run mutated-native --image=nginx:1.27-alpine --restart=Never --overrides='{"spec":{"containers":[{"name":"mutated-native","image":"nginx:1.27-alpine","resources":{"requests":{"cpu":"25m","memory":"32Mi"}}}]}}'
pod/mutated-native created
$ kubectl -n default get pod mutated-native -o jsonpath='{.metadata.labels}{"\n"}'
{"managed-by":"platform","run":"mutated-native","topology.kubernetes.io/zone":"zone-b"}
$ kubectl -n default delete pod mutated-native --ignore-not-found
pod "mutated-native" deleted from default namespace
$ kubectl delete mutatingadmissionpolicybinding add-platform-label-binding
mutatingadmissionpolicybinding.admissionregistration.k8s.io "add-platform-label-binding" deleted
$ kubectl delete mutatingadmissionpolicy add-platform-label
mutatingadmissionpolicy.admissionregistration.k8s.io "add-platform-label" deletedmanaged-by: platform and no mutating webhook exists. If the API is not served on this cluster, say so and name the feature gate; that is the honest answer to the same task on an older cluster.Self-check
A "require requests" policy never fires in a tenant namespace. Why?
Its LimitRange defaults the requests during mutating admission, before the validating webhook runs, so the object Kyverno inspects already complies. Admission order (mutate then validate) is the whole explanation, and it generalizes to every defaulting mechanism.
failurePolicy: Fail versus Ignore: argue both, then say what bounds the risk.
Fail: no unvalidated writes, at the cost of the API server refusing writes when the webhook is down (a cluster-wide outage from a policy pod). Ignore: writes keep flowing, at the cost of a bypass window. Bound it with a tight namespaceSelector, low timeoutSeconds, exemptions for system namespaces, and running the engine with enough replicas that it is not a single point of failure.
You tighten a policy to Deny. What happens to the workloads that already violate it?
Nothing: admission is write-time. They keep running until their next write (a rollout, a scale, a controller re-create), and then they fail, possibly at 3am. That is why the Audit → report → fix → Enforce sequence exists, and why you check the reports before flipping the dial.
Which engine would you pick to also create a default NetworkPolicy in every new namespace?
Kyverno: generation is a first-class capability there (generate rules that create and keep resources in sync). Gatekeeper validates and can mutate but does not generate; ValidatingAdmissionPolicy only validates. Matching capability to requirement is the whole question.
Why might a platform team prefer ValidatingAdmissionPolicy for a simple rule?
No extra controller, no webhook, no availability dependency, no version skew; the API server evaluates CEL in-process. The trade is a narrower feature set (no mutation in older versions, no generation, no reporting), so it suits simple invariants rather than a governance program.
A Kyverno ValidatingPolicy denies bare pods but a Deployment with the same violating template is admitted. Field?
spec.autogen.podControllers.controllers listing deployments (and statefulsets, daemonsets, jobs, cronjobs as needed). Autogen rewrites the Pod rule to match the controller's spec.template. Without it the Deployment passes and its pods are rejected one level down at the ReplicaSet, the same indirection as PSS. Note autogen and autogen.validatingAdmissionPolicy are mutually exclusive.
Which two Kyverno controller flags must be on before a PolicyException has any effect, and how does the report show an applied exception?
--enablePolicyException=true and --exceptionNamespace=<namespace> (only exceptions created in that namespace count). When applied, the PolicyReport records the rule as skip with the exception's name in the result properties, in both admission and background modes.
A Gatekeeper Constraint is applied, the violating object is still admitted, and status.totalViolations grows. Two likely settings?
spec.enforcementAction: dryrun (or warn) on the Constraint, or the webhook running with failurePolicy: Ignore (Gatekeeper's install default) while the webhook pod is unhealthy or timing out. Audit still records violations in both cases, which is why the count grows without any denial. Set deny, and check the webhook's failurePolicy and timeoutSeconds.
You write a ValidatingAdmissionPolicy with a misspelt field path. Is it rejected? Where do you find out, and what decides the runtime effect?
It is accepted; CEL type errors appear in the policy's status.typeChecking after creation, not as an apply error. At runtime the bad expression errors, and failurePolicy decides: Fail denies every matching request (with the binding's validationActions), Ignore lets them through. Read the status, then fix the path.
Docs to know your way around
- kyverno.io: ValidatingPolicy/MutatingPolicy references and the policy library.
- open-policy-agent.github.io/gatekeeper: ConstraintTemplate walkthrough; the gatekeeper-library repo for adaptable Rego.
- kubernetes.io: Validating Admission Policy, and Dynamic Admission Control for the webhook plumbing (
failurePolicy, selectors, reinvocation). - Offline:
kubectl explain validatingpolicy.spec,kubectl explain validatingadmissionpolicy.spec.validations,kubectl get validatingwebhookconfigurations -o yaml. - kyverno.io/docs/policy-types/overview: the version table and deprecation schedule; /policy-types/validating-policy for
evaluation,webhookConfigurationandautogen; /guides/exceptions for the two flags. - open-policy-agent.github.io/gatekeeper/website/docs/howto (match fields), /mutation, /enforcement-points and /failing-closed: the pages a Gatekeeper task turns on.
- kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy and /mutating-admission-policy: field lists,
parameterNotFoundAction, the CEL variables; /extensible-admission-controllers for webhookfailurePolicy,timeoutSeconds,sideEffects,matchConditions.