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 under admissionregistration.k8s.io/v1 and 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

  1. A platform team writes a MutatingAdmissionPolicy: spec.matchConstraints bounds which requests it can see, and spec.mutations lists the CEL-driven changes (each with a patchType of ApplyConfiguration or JSONPatch).
  2. As with the validating policy, the policy is inert until a MutatingAdmissionPolicyBinding references it; the binding narrows scope (matchResources) and supplies any parameter resource (paramRef). Note there is no validationActions analogue — a mutation has no Warn/Audit mode; it either applies or it does not.
  3. On a matching create/update, the compiled-in MutatingAdmissionPolicy admission plugin (one of the Admission Controllers) evaluates the CEL mutation in-process. CEL expressions see object, oldObject, request, params, namespaceObject, variables, and authorizer.
  4. For ApplyConfiguration, the CEL result is a partial object merged via Server-Side Apply. For JSONPatch, the CEL result is a JSON Patch applied directly.
  5. 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 with reinvocationPolicy: IfNeeded there 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.
  6. Reinvocation applies just as it does for mutating webhooks: the policy’s spec.reinvocationPolicy (Never or IfNeeded) 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 with IfNeeded must therefore be idempotent, because it may run twice; the matchConditions guard below is how you make it so.
  7. 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: enabled

Field-by-field:

  • mutations[].patchTypeApplyConfiguration (recommended; structured SSA merge, cannot clobber atomic fields) or JSONPatch (precise, RFC 6902). For JSONPatch, the CEL expression returns an array of JSONPatch{op, from, path, value} values, e.g. [JSONPatch{op: "add", path: "/spec/initContainers/-", value: ...}]; use the jsonpatch.escapeKey() CEL helper for path segments containing / or ~, which RFC 6902 requires to be escaped as ~1 and ~0 (Mutating Admission Policy reference).
  • mutations[].applyConfiguration.expression — a CEL expression returning a partial object via the Object{...} constructor syntax. The example builds a Pod fragment containing one init container with restartPolicy: Always — i.e. a native sidecar.
  • matchConditions — CEL guards that make the policy idempotent: here it skips Pods that already carry the mesh-proxy container, so reinvocation never injects a duplicate.
  • reinvocationPolicyIfNeeded re-runs the policy if another mutating step changed the object afterwards; Never runs it once. Same semantics as a mutating webhook’s reinvocationPolicy.
  • 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 when failurePolicy: 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 a matchConditions guard will inject it twice. The guard is mandatory for any append-style mutation.
  • JSONPatch index fragility. A JSONPatch mutation 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. ApplyConfiguration avoids 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 JSONPatch for 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, Priority are 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 gate MutatingAdmissionPolicy on by default — Cloudsmith 1.34), and reached stable / GA in Kubernetes 1.36 under admissionregistration.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 ApplyConfiguration mode plus matchConditions idempotency guards reproduce what mesh injectors do today, without the injector Deployment.

See Also