K8s Dynamic Admission Control: Mutating Admission Policies
Why it matters? | The big picture | Configuration | Use cases | User Workflows | Challenges | What's next?
Why it matters?
Kubernetes now provides native support for mutating resources during the admission phase as an alternative to mutating admission webhooks.
Introducing:
- An in-tree (aka in-process) framework
- Mutations declared in CEL
- Mutations applied by either "ApplyConfiguration" (uses server side apply merge strategy) or "JSONPatch"
- Flexible configuration
- Match constraints, match conditions, composition variables (available in alpha), and failure policies
- Parameterization
Important: With mutating admission policies added, the the mutating admission plugin order will become:
- Mutating admission controllers(e.g. DefaultIngressClass, DefaultStorageClass, etc)
- Mutating admission policies (introduced within this alpha feature and ordered randomly but keep the same random order while reinvocation)
- Mutating admission webhooks (ordered lexicographically by webhook name)
The big picture
Mutating Admission Policies was released as alpha in 1.32. The beta release is in progress and can be followed by the SIG API Machineery KEP 3962.
This new feature is a continuation of the previously released Validating Admission Policies (VAP) which has similarity in the API shape and configuration. I'll explore VAP in my next blog post.
Configuration
Find a lot more details in these resources: K8s docs | KEP 3962 | Source code | Kubecon NA 2024 Session | K8s CEL Docs | CEL Spec | External CEL macro assistance
Policy resources:
- MutatingAdmissionPolicy
- MutatingAdmissionPolicyBinding
- A parameter resource (optional)
- BYO-CRD (bring your own CRD)
- ConfigMap
Note: I'll explore paramaterizing policies in the Validating Mutating Policies blog post.
Example MutatingAdmissionPolicy Resource
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
name: "add-required-namespace-labels"
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
failurePolicy: Fail
reinvocationPolicy: IfNeeded
mutations:
- patchType: "ApplyConfiguration"
applyConfiguration:
expression: >
Object{
metadata: Object.metadata{
labels: Object.metadata.labels{
type : "user-workload",
systemid : object.metadata.name.split('-')[0],
}
}
}
Example MutatingAdmissionPolicyBinding Resource
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicyBinding
metadata:
name: "add-required-namespace-labels-binding"
spec:
policyName: "add-required-namespace-labels"
Use Cases
The mutation requirements below are illustrative and were created to test MAP features against cluster resources.
Mutations requirements:
- Add the following namespace labels to ALL user namespaces:
- systemid:
- nstype: user
- systemid:
- Mutate (default) the container's imagePullPolicy to "Always" for pods with label
platform: true
only. - Remove the
resources.request.limits
for ALL containers that include limits.
User workflows
Access a free kubernetes playground here: Fire up a ephemeral kubernetes mutating admission policies playground.
Apply the preexisting Mutating Admission Policies and Bindings to the cluster under the policy/
directory. Feel free to explore the policies for understanding.
> kubectl apply -f policy/
mutatingadmissionpolicybinding.admissionregistration.k8s.io/add-required-namespace-labels-binding created
mutatingadmissionpolicy.admissionregistration.k8s.io/add-required-namespace-labels created
mutatingadmissionpolicybinding.admissionregistration.k8s.io/image-pull-policy-binding created
mutatingadmissionpolicy.admissionregistration.k8s.io/image-pull-policy created
mutatingadmissionpolicybinding.admissionregistration.k8s.io/remove-resources-limits-binding created
mutatingadmissionpolicy.admissionregistration.k8s.io/remove-resources-limits created
Test mutation requirement 1
Create a namespace using this pattern: <systemid>-<service-name>-<environment>
. For example: 54321-myservice-prod
- systemid: 5 digit unique identifier
- service-name: Alphanumeric string
- environment: dev, test, or prod
❯ kubectl create ns 54321-myservice-prod
namespace/54321-myservice-prod created
❯ kubectl get ns 54321-myservice-prod -oyaml
apiVersion: v1
kind: Namespace
metadata:
creationTimestamp: "2025-01-14T01:06:52Z"
labels:
kubernetes.io/metadata.name: 54321-myservice-prod
systemid: "54321" # mutated by the MAP
type: user-workload # mutated by the MAP
name: 54321-myservice-prod
resourceVersion: "886"
uid: 323287ce-c16c-4e1e-ad14-d8bd2eec1413
spec:
finalizers:
- kubernetes
status:
phase: Active
Test mutation requirement 2
Create two pods, one with the platform: true
label, the other without the label.
❯ kubectl run mutate-image-pull --image nginx:alpine --labels=platform=true
pod/mutate-image-pull created
❯ kubectl run no-mutate-image-pull --image nginx:alpine
pod/no-mutate-image-pull created
Observe the running pod's output and verify the pod with the platform label was mutated to imagePullPolicy: Never
.
❯ kubectl get pods -o yaml | yq '.items[].spec.containers'
- image: nginx:alpine
imagePullPolicy: Never
name: mutate-image-pull
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-trxhw
readOnly: true
- image: nginx:alpine
imagePullPolicy: IfNotPresent
name: no-mutate-image-pull
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-877dm
readOnly: true
Test mutation requirement 3
Explore and apply the multi-container-resource-pod.yaml manifest to the cluster found in the resources/
directory.
❯ kubectl apply -f resources/multi-container-resource-pod.yaml
pod/multi-container-resource-pod created
Observe and validate the container named c1
's resources.limits.cpu
field was removed.
❯ kubectl get pods multi-container-resource-pod -oyaml |yq '.spec.containers[].resources'
limits:
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
limits:
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
{}
Challenges
Challenge 1
I faced a few challenges getting started with the new feature while following the K8s docs.
Note: per their documentation it does seem that the error aligns to the ApplyConfiguration patchType statement
Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of values not included in the apply configuration.
Policy and binding
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicy
metadata:
name: "sidecar-policy.example.com"
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
reinvocationPolicy: IfNeeded
mutations:
- patchType: "ApplyConfiguration"
applyConfiguration:
expression: >
Object{
spec: Object.spec{
initContainers: [
Object.spec.initContainers{
name: "mesh-proxy",
image: "mesh/proxy:v1.0.0",
args: ["proxy", "sidecar"],
restartPolicy: "Always"
}
]
}
}
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: MutatingAdmissionPolicyBinding
metadata:
name: "sidecar-binding-test.example.com"
spec:
policyName: "sidecar-policy.example.com"
$ kubectl apply -f pod.yaml
The pods "p1" is invalid: : policy 'sidecar-policy.example.com' with binding 'sidecar-binding-test.example.com' denied request: error applying patch: invalid ApplyConfiguration: may not mutate atomic arrays, maps or structs: .spec.initContainers[0].args
The way I did get to get their example to work is to use the JSONPatch patchType.
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add", path: "/spec/initContainers/-",
value: Object.spec.initContainers{
name: "mesh-proxy",
image: "mesh-proxy/v1.0.0",
command: ["bin/sh", "-c"],
restartPolicy: "Always"
}
}
]
Challenge 2
Implementing container-level security context mutations required complex CEL expressions to properly incorporate Pod-level security context requirements.
- add default container secure securityContext's
- defaulting (mutating) pod or container securityContext settings is a bit more complex due to having so many variations of securtyContext settings to match on the incoming request object. I played around with it for a while but ultimately requires more effort to account for all the variations. I'll leave this for a future effort, maybe when the MAP feature is in Beta.
What's Next
- Keep an close eye and participation on beta progress
- Beta feature to watch for: Safety
- A lot more testing is required, e.g. against other resource types, parameterization, load testing
- Deeper CEL exploration and understanding is also needed