Mutating Admission Policy in Kubernetes
Required knowledge for the CKS certification.
MutatingAdmissionPolicy is a declarative, in-process alternative to mutating admission webhooks, stable and enabled by default as of Kubernetes v1.36. Where a MutatingWebhookConfiguration requires a running webhook server, valid TLS certificates, and a network round-trip on every matching request, a MutatingAdmissionPolicy evaluates CEL expressions directly inside the API server process. No external server, no network hop.
Security teams use MutatingAdmissionPolicy to enforce defaults that workload authors may omit: classification labels, required sidecar containers, or missing annotations that downstream network policies and audit rules depend on. Because the policy runs at admission time — before the object is persisted — mutations are applied before the object is visible to any controller or workload.
1. How MutatingAdmissionPolicy Differs from Webhooks
Before: Enforcing a mutation at admission time required a MutatingWebhookConfiguration pointing to a running webhook server. That server needed a valid TLS certificate, a Service and Deployment in the cluster, and a startup sequence that guaranteed availability before the API server needed it. An unavailable webhook server, depending on its failurePolicy, could block all matching admission requests.
After: MutatingAdmissionPolicy defines mutations as CEL expressions stored in a Kubernetes API object. The API server evaluates them in-process. No webhook server, no TLS setup, no network round-trip.
The tradeoff: CEL's expression language is intentionally restricted. Mutations that require external state lookups, complex imperative logic, or access to resources outside the CEL variable set still require a webhook.
2. Policy Components
A complete policy requires two resources. A third (the parameter resource) is optional.
| Resource | Purpose |
|---|---|
MutatingAdmissionPolicy | Defines the mutation logic using CEL expressions |
MutatingAdmissionPolicyBinding | Binds the policy to a scope (namespaces, resource selectors) and optionally references a parameter object |
| Parameter resource | Any Kubernetes object type; provides configuration available in CEL via the params variable |
The parameter resource is only required when spec.paramKind is set in the policy. Without it, the policy uses only the variables derived from the incoming object itself.
3. Mutation Types
Mutations are declared as a list under spec.mutations. Each entry specifies a patchType of either ApplyConfiguration or JSONPatch.
ApplyConfiguration
Uses the server-side apply merge strategy. The CEL expression evaluates to a partial resource object constructed with Object{} initializers. Fields absent from the expression are left unchanged on the incoming resource. ApplyConfiguration cannot modify atomic structs, maps, or arrays.
mutations:
- patchType: "ApplyConfiguration"
applyConfiguration:
expression: >
Object{
spec: Object.spec{
initContainers: [
Object.spec.initContainers{
name: "security-proxy",
image: "registry.example.com/security-proxy:v1.0.0",
args: ["proxy", "sidecar"],
restartPolicy: "Always"
}
]
}
}
JSONPatch
The CEL expression evaluates to an array of JSONPatch values. Supports standard operations: add, remove, replace, move, copy, test. Use jsonpatch.escapeKey() for path segments that contain / or ~.
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add",
path: "/metadata/labels/" + jsonpatch.escapeKey("security.example.com/classification"),
value: "internal"
}
]
4. Security Use Cases
Inject a Security Sidecar
This policy injects a security proxy initContainer into every Pod that does not already carry one. A matchConditions expression prevents double-injection on re-admission.
MutatingAdmissionPolicy:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: "sidecar-injection.security.example.com"
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
matchConditions:
- name: does-not-already-have-sidecar
expression: "!object.spec.initContainers.exists(ic, ic.name == 'security-proxy')"
failurePolicy: Fail
reinvocationPolicy: IfNeeded
mutations:
- patchType: "ApplyConfiguration"
applyConfiguration:
expression: >
Object{
spec: Object.spec{
initContainers: [
Object.spec.initContainers{
name: "security-proxy",
image: "registry.example.com/security-proxy:v1.0.0",
args: ["proxy", "sidecar"],
restartPolicy: "Always"
}
]
}
}
MutatingAdmissionPolicyBinding:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: "sidecar-injection-binding"
spec:
policyName: "sidecar-injection.security.example.com"
matchResources:
namespaceSelector:
matchLabels:
sidecar-injection: "enabled"
The binding restricts injection to namespaces carrying the sidecar-injection: "enabled" label, making rollout gradual and auditable. Label each namespace when it is ready to receive the sidecar, rather than enabling the policy cluster-wide at once.
Verify the mutation applies:
kubectl run test-pod --image=nginx -n <labeled-namespace> --dry-run=server -o yaml | \
grep -A5 initContainers
The --dry-run=server flag sends the request through the full admission chain, including active MutatingAdmissionPolicy objects, and returns the mutated object without persisting it.
Enforce Security Classification Labels
This policy uses JSONPatch to add a required classification label to every Pod that is missing one. Labels are used by downstream NetworkPolicy, RBAC rules, and audit log queries that select on workload classification.
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: "enforce-classification.security.example.com"
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchConditions:
- name: missing-classification
expression: >
!has(object.metadata.labels) ||
!('security.example.com/classification' in object.metadata.labels)
failurePolicy: Fail
reinvocationPolicy: IfNeeded
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add",
path: "/metadata/labels/" + jsonpatch.escapeKey("security.example.com/classification"),
value: "internal"
}
]
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: "enforce-classification-binding"
spec:
policyName: "enforce-classification.security.example.com"
matchResources: {}
An empty matchResources in the binding means the policy applies cluster-wide, to all namespaces.
5. Failure and Reinvocation Policies
failurePolicy
| Value | Behavior |
|---|---|
Fail | The admission request is rejected if the policy cannot be evaluated. Use for security-critical mutations. |
Ignore | The request proceeds even if the policy fails to evaluate. Use only for best-effort mutations that must never block admission. |
For any policy intended to enforce a security invariant, set failurePolicy: Fail. A CEL evaluation error with Ignore silently skips the mutation — the object is admitted without it.
reinvocationPolicy
When multiple admission plugins modify the same object in sequence, a later mutation may change a field that an earlier policy already evaluated. reinvocationPolicy: IfNeeded causes the policy to re-run after any subsequent mutation to the same object.
| Value | Behavior |
|---|---|
IfNeeded | Re-evaluate the policy if a later admission plugin modifies the object. Ensures the mutation takes effect even when plugin ordering is non-deterministic. |
Never | The policy runs exactly once per request. |
Use IfNeeded for sidecar injection and label enforcement, where the presence of the mutation matters regardless of what other policies run after.
6. CEL Variables in Mutation Expressions
| Variable | Description |
|---|---|
object | The incoming resource object (null for DELETE requests) |
oldObject | The existing resource object before the operation (null for CREATE requests) |
request | API request attributes: user, groups, resource kind, verb, namespace |
params | The parameter resource referenced by the binding's paramRef field |
namespaceObject | The Namespace object for namespaced resources (null for cluster-scoped resources) |
variables | Map of pre-computed variables defined in spec.variables for expression reuse |
authorizer | CEL Authorizer that can perform authorization checks inside the expression |
7. RBAC for Policy Management
MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding are cluster-scoped resources in the admissionregistration.k8s.io group. A principal that can write these resources controls which mutations run at admission time across all matching namespaces. Restrict this access explicitly:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: admission-policy-admin
rules:
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingadmissionpolicies", "mutatingadmissionpolicybindings"]
verbs: ["create", "update", "delete", "patch"]
Audit who holds this ClusterRole:
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name == "admission-policy-admin") | {binding: .metadata.name, subjects: .subjects}'
Apply the same audit to any ClusterRole that grants * verbs on admissionregistration.k8s.io resources.
8. Auditing Active Policies
List all active policies and their bindings:
kubectl get mutatingadmissionpolicies
kubectl get mutatingadmissionpolicybindings
Inspect the full mutation logic and match conditions for a specific policy:
kubectl describe mutatingadmissionpolicy <policy-name>
Test whether a specific policy's mutations are applied to a new object by using server-side dry-run:
kubectl run verify --image=nginx --dry-run=server -o yaml
The server processes the dry-run request through all active admission plugins, including MutatingAdmissionPolicy objects, and returns the mutated object as it would be stored. This is the authoritative way to confirm a mutation takes effect before deploying the policy to production.

References
This article is based on information from the following official sources:
- Mutating Admission Policy - Kubernetes
- Kubernetes v1.36 Release - Kubernetes Blog