MutatingAdmissionPolicy
A MutatingAdmissionPolicy is the mutation-side sibling of ValidatingAdmissionPolicy: an in-process, declarative way to modify an API object during admission using Common Expression Language (CEL), with no external webhook in the request path (Kubernetes — Mutating Admission Policy). Where ValidatingAdmissionPolicy can only accept or reject, MutatingAdmissionPolicy can rewrite the object — defaulting fields, adding labels, and even injecting sidecar containers — entirely inside
kube-apiserver. It exists to remove the operational fragility of mutating webhooks (the external Deployment, the TLS certificate rotation, the cluster-wedge risk) for the common defaulting and labelling cases. MutatingAdmissionPolicy reached GA (stable) in Kubernetes 1.36 (released 2026-04-22), served underadmissionregistration.k8s.io/v1and enabled by default, after alpha in 1.32 and beta in 1.34 (Kubernetes — Mutating Admission Policy, KEP 3962).
Mental Model
The whole point is the same as ValidatingAdmissionPolicy’s — trade arbitrary logic for operational safety — but on the mutating side. A MutatingAdmissionWebhook can do anything, but it is an external service whose unavailability can deadlock the cluster and whose certificate is a standing liability. MutatingAdmissionPolicy moves the common mutations (set a default, add a label, inject a known sidecar) inside the apiserver as CEL, where they cannot hang and cannot be unreachable.
The conceptually new piece versus the validating policy is how the mutation is expressed. There are two modes:
ApplyConfiguration— the CEL expression evaluates to a partial object, which the apiserver merges into the real object using Server-Side Apply semantics. This is the recommended mode: SSA’s structured merge means the policy adds a container or label without risking accidental deletion of fields it did not mention. ApplyConfiguration deliberately cannot overwrite atomic structs/maps/arrays wholesale — a guardrail against clobbering.JSONPatch— the CEL expression evaluates to an RFC 6902 JSON Patch, the same patch format a mutating webhook would emit. More precise, more dangerous (hard-coded array indices, full-array replacement) — used when ApplyConfiguration’s merge semantics are not what you want.
flowchart TD P[MutatingAdmissionPolicy<br/>spec.matchConstraints<br/>spec.mutations CEL<br/>spec.reinvocationPolicy] -->|policyName| B[MutatingAdmissionPolicyBinding<br/>spec.matchResources<br/>spec.paramRef] B --> M{Request matches?} M -->|yes| EVAL[Evaluate CEL mutation<br/>in-process] EVAL --> T{patchType} T -->|ApplyConfiguration| SSA[Server-Side Apply merge<br/>structured, no clobber] T -->|JSONPatch| JP[Apply RFC 6902 patch] SSA --> OUT[Mutated object] JP --> OUT M -->|no| SKIP[Skip] style EVAL fill:#e8f0ff
MutatingAdmissionPolicy flow. The insight: the two patchType modes are a safety/precision trade — ApplyConfiguration merges via Server-Side Apply and cannot accidentally clobber fields, while JSONPatch is precise but carries the same index-fragility risks as a webhook patch. Prefer ApplyConfiguration.
Mechanical Walk-through
- A platform team writes a
MutatingAdmissionPolicy:spec.matchConstraintsbounds which requests it can see, andspec.mutationslists the CEL-driven changes (each with apatchTypeofApplyConfigurationorJSONPatch). - As with the validating policy, the policy is inert until a
MutatingAdmissionPolicyBindingreferences it; the binding narrows scope (matchResources) and supplies any parameter resource (paramRef). Note there is novalidationActionsanalogue — a mutation has noWarn/Auditmode; it either applies or it does not. - On a matching create/update, the compiled-in
MutatingAdmissionPolicyadmission plugin (one of the Admission Controllers) evaluates the CEL mutation in-process. CEL expressions seeobject,oldObject,request,params,namespaceObject,variables, andauthorizer. - For
ApplyConfiguration, the CEL result is a partial object merged via Server-Side Apply. ForJSONPatch, the CEL result is a JSON Patch applied directly. - Where this sits in the mutating pass. The KEP fixes the high-level order of the three mutating mechanisms: built-in mutating controllers (
DefaultStorageClass,ServiceAccount, …) run first, then mutating admission policies, then mutating admission webhooks (KEP 3962 README). So a policy sees the object after the built-in controllers have defaulted it but before any external webhook touches it — a deliberate choice so that the cheap, safe in-process step happens ahead of the network hop. Ordering among policies (and the reinvocation passes) carries no guarantee: the API reference states explicitly that withreinvocationPolicy: IfNeededthere is “no guarantee of order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies” (Mutating Admission Policy reference). Write mutations to be order-independent. - Reinvocation applies just as it does for mutating webhooks: the policy’s
spec.reinvocationPolicy(NeverorIfNeeded) controls whether the policy is re-evaluated after some other mutating admission step changed the object. Crucially, this reinvocation is cross-mechanism — a policy and a webhook share one reinvocation budget: “the mutating policies are rerun if a mutating webhook or mutation policy modifies an object” (KEP 3962). A policy withIfNeededmust therefore be idempotent, because it may run twice; thematchConditionsguard below is how you make it so. - The mutated object then proceeds to schema validation and validating admission as usual.
Configuration / API Surface
# The policy: an in-process CEL mutation that injects a mesh sidecar
apiVersion: admissionregistration.k8s.io/v1 # GA group/version as of k8s 1.36
kind: MutatingAdmissionPolicy
metadata:
name: sidecar-injector.example.com
spec:
paramKind:
apiVersion: mutations.example.com/v1
kind: Sidecar
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
matchConditions: # CEL guard: skip Pods that already have the sidecar
- name: not-already-injected
expression: '!object.spec.initContainers.exists(c, c.name == "mesh-proxy")'
failurePolicy: Fail
reinvocationPolicy: IfNeeded # re-run if a later mutation changes the object
mutations:
- patchType: ApplyConfiguration # ApplyConfiguration | JSONPatch
applyConfiguration:
expression: >
Object{
spec: Object.spec{
initContainers: [
Object.spec.initContainers{
name: "mesh-proxy",
image: "mesh/proxy:v1.0.0",
restartPolicy: "Always" # a native sidecar container
}
]
}
}
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: sidecar-injector-prod.example.com
spec:
policyName: sidecar-injector.example.com
matchResources:
namespaceSelector:
matchLabels:
mesh-injection: enabledField-by-field:
mutations[].patchType—ApplyConfiguration(recommended; structured SSA merge, cannot clobber atomic fields) orJSONPatch(precise, RFC 6902). ForJSONPatch, the CEL expression returns an array ofJSONPatch{op, from, path, value}values, e.g.[JSONPatch{op: "add", path: "/spec/initContainers/-", value: ...}]; use thejsonpatch.escapeKey()CEL helper for path segments containing/or~, which RFC 6902 requires to be escaped as~1and~0(Mutating Admission Policy reference).mutations[].applyConfiguration.expression— a CEL expression returning a partial object via theObject{...}constructor syntax. The example builds a Pod fragment containing one init container withrestartPolicy: Always— i.e. a native sidecar.matchConditions— CEL guards that make the policy idempotent: here it skips Pods that already carry themesh-proxycontainer, so reinvocation never injects a duplicate.reinvocationPolicy—IfNeededre-runs the policy if another mutating step changed the object afterwards;Neverruns it once. Same semantics as a mutating webhook’sreinvocationPolicy.- No
validationActions— there is no non-enforcing mode; a mutation is applied or not. (You cannot “warn” about a mutation.)
The served group/version tracked the maturity ladder: admissionregistration.k8s.io/v1alpha1 in 1.32, v1beta1 in 1.34, and v1 from the GA in 1.36 (KEP 3962 README). On a mixed-version fleet, confirm the served version with kubectl api-resources | grep mutatingadmissionpolicy before pinning an apiVersion string, since a node pool still on 1.34 will only serve v1beta1.
Failure Modes
- CEL mutation error under
failurePolicy: Fail. A CEL expression that errors (missing field, type mismatch) rejects the request whenfailurePolicy: Fail. Defensive CEL (has(...)guards) is required, exactly as for ValidatingAdmissionPolicy. - Non-idempotent mutation + reinvocation = double application. With
reinvocationPolicy: IfNeeded, a policy that appends a container without amatchConditionsguard will inject it twice. The guard is mandatory for any append-style mutation. - JSONPatch index fragility. A
JSONPatchmutation with hard-coded array indices can patch the wrong element if an earlier mutation reindexed the array — the same footgun as a webhook JSON Patch.ApplyConfigurationavoids this by merging structurally. - Cannot read external data. CEL has no network. Mutations that need data from outside the request (a value fetched from an external system, a generated secret) cannot be MutatingAdmissionPolicy — that still requires a MutatingAdmissionWebhook.
- Cannot mutate atomic fields with ApplyConfiguration. By design, ApplyConfiguration refuses to overwrite atomic structs/maps/arrays — a safety property, but a surprise if you expected to replace such a field. Use
JSONPatchfor a deliberate full replacement.
Alternatives and When to Choose Them
- vs MutatingAdmissionWebhook. MutatingAdmissionPolicy wins on operations: in-process, no certs, no extra Deployment, no wedge risk, lower latency. The webhook wins on capability: arbitrary code, external data. The intended trajectory is MutatingAdmissionPolicy absorbing the common defaulting/labelling/sidecar-injection cases, with webhooks retained only where external data or arbitrary logic is genuinely needed.
- vs built-in mutators.
ServiceAccount,DefaultStorageClass,Priorityare compiled-in and need no policy. MutatingAdmissionPolicy is for custom defaulting those plugins do not provide. - vs ValidatingAdmissionPolicy. Same CEL machinery, opposite intent: validating rejects, mutating fixes. If a bad value should be corrected rather than refused, use the mutating policy; if an invariant must be enforced, use the validating one. They are frequently paired — mutate to default, then validate the result.
Production Notes
- Stability timeline. MutatingAdmissionPolicy was alpha in Kubernetes 1.32 (
admissionregistration.k8s.io/v1alpha1), beta in 1.34 (admissionregistration.k8s.io/v1beta1, feature gateMutatingAdmissionPolicyon by default — Cloudsmith 1.34), and reached stable / GA in Kubernetes 1.36 underadmissionregistration.k8s.io/v1, enabled by default (Mutating Admission Policy reference, KEP 3962 README). Each tier added one served version while keeping the prior ones for one release of overlap, the usual API-promotion cadence. - The strategic picture. With both ValidatingAdmissionPolicy (GA 1.30) and MutatingAdmissionPolicy (GA 1.36) stable, Kubernetes now has a complete in-process, CEL-based admission story covering both validation and mutation. The long-term intent is that most policy needs currently met by OPA Gatekeeper, Kyverno, or bespoke webhooks can move to native CEL policies — eliminating an entire class of operational risk (the webhook Deployment and its certificate). Webhooks remain for the genuinely-needs-external-data minority.
- Native sidecar injection via MutatingAdmissionPolicy is an explicit design use case: the
ApplyConfigurationmode plusmatchConditionsidempotency guards reproduce what mesh injectors do today, without the injector Deployment.
See Also
- ValidatingAdmissionPolicy — the validation-side sibling, same CEL machinery
- CEL in Kubernetes — the expression language MutatingAdmissionPolicy is built on
- MutatingAdmissionWebhook — the external-webhook alternative this feature replaces for common cases
- Server-Side Apply — the merge semantics behind the
ApplyConfigurationmutation mode - Admission Controllers — the compiled-in
MutatingAdmissionPolicyplugin that evaluates these - Sidecar Containers — native sidecar injection is a primary use case
- API Server Request Flow — MutatingAdmissionPolicy runs in the mutating-admission stage
- OPA Gatekeeper / Kyverno — webhook-based policy engines the CEL policies aim to displace
- Kubernetes MOC — parent map (§12 Security)