Mutating Admission Policy Part 1
Hello there!
It’s been some time since the last article.
Since march, we played with different kind of native admission controller, so that we could manage guardrails, or policies, or whatever we want to call it, in kubernetes.
This article will be a continuation of those last articles and we’ll have a look at the recently GA Mutating Admission Policy. Also, And it was planned at the beginning, it will be a 2 part article, because, well, there is a lot to say
Our agenda will be as follow:
- What is Mutating Admission Policy
- Creating basic
MAP - Combining all of our options to secure a kubernetes cluster
1. What is Mutating Admission Policy
Similarly to ValidatingAdmissionPolicy, that I call VAP (because I’m lazy, told you last time 😏), we also have
MutatingAdmissionPolicy, that I’ll call MAP in all future reference, without surprise.
As one could expect, MAP are a (native) way to mutate object that are send to the API server, before those are created.
Taking again the Azure policy comparison, it would be the custom policies with modify, or deployifnotexist scenarios.
Attentive readers will notice that there is a preview for Azure policies with mutate effect, but that’s outside of our scope… today 😎
For a long time, it was not something that we could do natively, and we had to relied on 3rd party tool such as kyverno. Now, I’m not saying that we should stop using kyverno here. It does its job quite well IMHO. But for people not already kyverno expert, it’s worth a look I guess.
If we try to draw how the MAP works, it’s very similar to the VAP.

The reference API is available here.
2. Creating basic MAPs
2.1. Undersrtanding a bit the MutatingAdmissionPolicy configuration
Last time we went directly into the creation of a VAP, without looking first into the API.
Let’s not do that this time 😆 and start by checking some information.
So basically, we’ll have the MAP and its own binding.
In the MAP we have the spec section in which everything is defined.
Without copy-pasting all the API reference, we can summarize.
failurePolicydefines how failures are handle. It can beFailorIgnorematchConstraintsspecifies what resources this policy is designed to validate. The example below shows how to usematchConstraintwithresourceRules. Other arguments are detailled in the API documentation. This specifcmatchConstraintsconfigure theMAPto trigger atnamespacecreation.
spec:
# Target only namespace creations
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
matchConditionsfilters requests already validated bymatchConstraints. As in the example below, we can for example add a condition on the namespaces with a specific label. In this specific section, we already define that the trigger occur at the creation of any namespace. We add a requirement that the namespace shoud have also the labelenv=prd.
# Condition : Apply only if the namespace has the label `env=prd`
matchConditions:
- name: "has-env-prd-label"
expression: >
has(object.metadata.labels) &&
'env' in object.metadata.labels &&
object.metadata.labels['env'] == 'prd'
paramKind, as inVAP, allows to manage parameters of theMAPindependantly. We can useConfigMapsorCRDsand add the reference in the binding.reinvocationPolicyallows to determine if the modification should occur once or more.variablesopen interesting possibilities to manipulates expressions inside theMAP. Think terraform locals if you need another something similar.mutations, at last, is where the modification are defined. The example below define a mutation that use the jsonPatch kind and that adds on namespaces some security related labels.
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1enforce",
value: "restricted"
},
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1enforce-version",
value: "v1.36"
}
]
On the path, the / acts as a delimitation to indicate subsection. Meaning that path: "/metadata/labels/something" with value: "this" would be in yaml
metadata:
labels:
something: "this"
And that’s where the ~1 is useful. It is understood by the MAP as the character /. Adding simply the / in the pod-security.kubernetes.io~1enforce would be translated as
metadata:
labels:
pod-security.kubernetes.io:
enforce: "restricted"
Apart from the JSONPatch, we can also leverage the applyConfiguration. We’ll have a look at this one later.
Below is our first sample MAP
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: enforce-restricted-pss-on-prd-namespaces
spec:
# Target only namespace creations
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
# Condition : Apply only if the namespace has the label `env=prd`
matchConditions:
- name: "has-env-prd-label"
expression: >
has(object.metadata.labels) &&
'env' in object.metadata.labels &&
object.metadata.labels['env'] == 'prd'
# Behavior on failure
failurePolicy: Fail
reinvocationPolicy: IfNeeded
# Mutation : Add labels for Pod Security Admission
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1enforce",
value: "restricted"
},
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1enforce-version",
value: "v1.36"
}
]
- patchType: "JSONPatch"
jsonPatch:
expression: >
has(object.metadata.annotations) ?
[
JSONPatch{
op: "add",
path: "/metadata/annotations/" + jsonpatch.escapeKey("policy.modifiedby.com/enforce-restricted-pss-on-prd-namespaces"),
value: "true"
}
]
:
[
JSONPatch{
op: "add",
path: "/metadata/annotations",
value: {"policy.modifiedby.com/" + "enforce-restricted-pss-on-prd-namespaces": "true"}
}
]
And its associated binding.
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: enforce-restricted-pss-on-prd-namespaces-binding
spec:
policyName: enforce-restricted-pss-on-prd-namespaces
matchResources:
namespaceSelector: {}
It’s quite straightforward to use the MutatingAdmissionPolicyBinding. We need to reference the MAP that we want to bind and the matchResource allows us to select, in this case, all namespaces. As for the VAP, this is also in the MutatingAdmissionPolicyBinding that we have the paramRef allowing us to referencde an CRD or a configMap to act as a source of parameters.
Now, detailing the path section, we have op: add which means that we are adding something in our manifest.
Then, we have the path: "/metadata/labels/pod-security.kubernetes.io~1enforce" and the value: "restricted".
All this means that the policy add the label pod-security.kubernetes.io~1enforce="restricted".
Notice the other JSONPatch mutation which is used to add annotions.
- patchType: "JSONPatch"
jsonPatch:
expression: >
has(object.metadata.annotations) ?
[
JSONPatch{
op: "add",
path: "/metadata/annotations/" + jsonpatch.escapeKey("policy.modifiedby.com/enforce-restricted-pss-on-prd-namespaces"),
value: "true"
}
]
:
[
JSONPatch{
op: "add",
path: "/metadata/annotations",
value: {"policy.modifiedby.com/" + "enforce-restricted-pss-on-prd-namespaces": "true"}
}
]
The idea here is to annotate the object targeted by the MAP (in this case a namespace, as you can read in the spec.matchConstraint) so that we keep track of the mutation.
There is also the has(object.metadata.annotations) ? followed by 2 JSONPatch CEL expression, delimited by a :.
That’s because we want to manage 2 scenarios here:
- if there is already an
annotationssection. That’s the first CEL expression. And that’s why the path is written with the trailing/in/metadata/annotations/. It means that we look in the annotation section and the/is here to define that we’ll go in theannotationssection, to addpolicy.modifiedby.com/enforce-restricted-pss-on-prd-namespaceswith the value"true". - The second CEL expression, after the
:manage the scenario where there is noannotationssection. By specifying thepath: "/metadata/annotations", without the trailing/we add theannotationssection, and the value is a json object{"policy.modifiedby.com/" + "enforce-restricted-pss-on-prd-namespaces": "true"}
2.2. Testing our policy
Now that everything is (a bit 😅) clearer, let’s try this policy.
We defined it so that each time a namespace with the label env=prd is created, it would had the labels for the Pod Security Standard Restricted level.
Let’s write a manifest for a namespace following this requirement.
apiVersion: v1
kind: Namespace
metadata:
name: checkmapns
labels:
env: prd
Upon creating the namespace, we can see that it does have the additional labels resulting of the mutation.
➜ ~ k get namespaces checkmapns -o json |jq '.metadata.labels'
{
"env": "prd",
"kubernetes.io/metadata.name": "checkmapns",
"pod-security.kubernetes.io/enforce": "restricted",
"pod-security.kubernetes.io/enforce-version": "v1.36"
}
We can also verify that there is an annotation to track the modification of our MAP
➜ ~ k get namespaces checkmapns -o json |jq '.metadata.annotations'
{
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Namespace\",\"metadata\":{\"annotations\":{},\"labels\":{\"env\":\"prd\"},\"name\":\"checkmapns\"},\"spec\":{},\"status\":{}}\n",
"policy.modifiedby.com/enforce-restricted-pss-on-prd-namespaces": "true"
}
If on the other hand we create another namespace without the env label, the policy is not triggered
➜ ~ k create namespace checkmapns2
➜ ~ k get ns $cil1 checkmapns2
NAME STATUS AGE
checkmapns2 Active 24s
➜ ~ k get ns $cil1 checkmapns2 -o yaml
apiVersion: v1
kind: Namespace
metadata:
creationTimestamp: "2026-07-03T12:41:43Z"
labels:
kubernetes.io/metadata.name: checkmapns2
name: checkmapns2
resourceVersion: "211196"
uid: 9de5abc1-924f-4033-a037-2dd334660083
spec:
finalizers:
- kubernetes
status:
phase: Active
And there is nothing in the annotations or the labels sections.
Ok let’s move on.
3. Combining all of our options to secure a kubernetes cluster
In this section, we’ll try to push further our usage of the MutatingAdmissionPolicies with a scenario.
3.1. Defining a scenario
The scenario, following our first sample in indeed quite simple (in what we need, not the implementation ^^).
We would like to ensure that the Pod Security Standard are applied on our namespaces and the pods that live in those.
We already have a MAP that can mutate the namespaces with the env=prd label.
We want to add a clear requirement:
- All namespaces with
env=prdlabel should be configured to enforce the PSS restricted. - We also add that the namespaces with env!=prd should be configure to enforce the PSS baseline, but also warn on the PSS restricted requirements for pods
First thing first, we should ensure that all namespace have the env label. We can achieve this with a VAP actually.
3.2. Write a VAP denying the creation of namespaces without env label defined.
We saw in the last article how to manage VAP. So let’s just write it, and it’s binding.
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-env-label-on-namespace
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["namespaces"]
validations:
- expression: >
has(object.metadata.labels)
&& "env" in object.metadata.labels
&& object.metadata.labels["env"] in ["dev", "prd", "ppr", "staging"]
message: "Namespace should have a valid 'env' defined (dev|prd|ppr|staging)."
reason: Invalid
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-env-label-on-namespace-binding
spec:
policyName: require-env-label-on-namespace
validationActions: [Deny]
matchResources:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["namespaces"]
We can validate that the ValidatingAdmissionPolicy is working be trying to create a new namespace, without the env=prd.
➜ ~ k create namespace checkvapns1 -o yaml
The namespaces "checkvapns1" is invalid: : ValidatingAdmissionPolicy 'require-env-label-on-namespace' with binding 'require-env-label-on-namespace-binding' denied request: Namespace should have a valid 'env' defined (dev|prd|ppr|staging).
3.3. Adding a MAP for the non prd namespace
We already wrote the MAP to add the required labels for PSS on prd namespace. So the non prd namespaces MAP is quite similar.
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: enforce-restricted-pss-on-non-prd-namespaces
spec:
# Target only namespace creations
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["namespaces"]
# Condition : Apply only if the namespace has the label `env` different from `prd`
matchConditions:
- name: "has-env-non-prd-label"
expression: >
has(object.metadata.labels) &&
'env' in object.metadata.labels &&
object.metadata.labels['env'] != 'prd'
# Behavior on failure
failurePolicy: Fail
reinvocationPolicy: IfNeeded
# Mutation : Add labels for Pod Security Admission
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
[
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1audit",
value: "restricted"
},
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1warn",
value: "restricted"
},
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1audit-version",
value: "v1.36"
},
JSONPatch{
op: "add", path: "/metadata/labels/pod-security.kubernetes.io~1warn-version",
value: "v1.36"
}
]
- patchType: "JSONPatch"
jsonPatch:
expression: >
has(object.metadata.annotations) ?
[
JSONPatch{
op: "add",
path: "/metadata/annotations/" + jsonpatch.escapeKey("policy.modifiedby.com/enforce-restricted-pss-on-non-prd-namespaces"),
value: "true"
}
]
:
[
JSONPatch{
op: "add",
path: "/metadata/annotations",
value: {"policy.modifiedby.com/enforce-restricted-pss-on-non-prd-namespaces": "true"}
}
]
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: enforce-restricted-pss-on-non-prd-namespaces-binding
spec:
policyName: enforce-restricted-pss-on-non-prd-namespaces
matchResources:
namespaceSelector: {}
The important part here is the matchConditions section which specify that the policy should apply only for namespaces with the label not equal to prd
matchConditions:
- name: "has-env-non-prd-label"
expression: >
has(object.metadata.labels) &&
'env' in object.metadata.labels &&
object.metadata.labels['env'] != 'prd'
Because we also have the VAP that accepts only the env values dev|ppr|prd|staging, it’s enough has a filter.
3.4. Inheriting the namespace env label
Now that we have some basics in terms of admission controllers on the namespaces, we would like to go to the next stpe, which is to manipulate admission for the pods.
Before diving into the ultimate aims which is to secure our pods, we’ll start with a first MAP that allows us to configure the env label inheritance from the namespace to the pod.
To attain this, we have to consider the trigger. So we’ll write a condition that is trigger for pod creation
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
With an additional matchConditions that check for pods without labels or without specifically the env label.
matchConditions:
- name: "has-label-and-env-label-is-absent"
expression: >
!has(object.metadata.labels) ||
!('env' in object.metadata.labels)
Let’s verify this by creating a bunch of namespaces.
apiVersion: v1
kind: Namespace
metadata:
name: testmap1
labels:
env: prd
spec: {}
status: {}
---
apiVersion: v1
kind: Namespace
metadata:
name: testmap2
labels:
env: dev
spec: {}
status: {}
---
apiVersion: v1
kind: Namespace
metadata:
name: testmap3
labels:
noenvdefined: thatslife
spec: {}
status: {}
---
apiVersion: v1
kind: Namespace
metadata:
name: testmap4
labels:
env: notavalidenv
spec: {}
status: {}
---
apiVersion: v1
kind: Namespace
metadata:
name: testmap5
labels:
env: ppr
spec: {}
status: {}
And with our full MAP
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: inherit-env-label-from-namespace
spec:
# Target only pod creations and updates
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
# Condition : Apply only if the namespace has the label `env` is absent
matchConditions:
- name: "has-label-and-env-label-is-absent"
expression: >
!has(object.metadata.labels) ||
!('env' in object.metadata.labels)
# Behavior on failure
failurePolicy: Fail
reinvocationPolicy: IfNeeded
# Mutation : Inherit the `env` label from the namespace to the pod
mutations:
- patchType: "JSONPatch"
jsonPatch:
expression: >
has(object.metadata.labels) ?
[
JSONPatch{
op: "add",
path: "/metadata/labels/env",
value: namespaceObject.metadata.labels['env']
}
]
:
[
JSONPatch{
op: "add",
path: "/metadata/labels",
value: {"env": namespaceObject.metadata.labels['env']}
}
]
- patchType: "JSONPatch"
jsonPatch:
expression: >
has(object.metadata.annotations) ?
[
JSONPatch{
op: "add",
path: "/metadata/annotations/" + jsonpatch.escapeKey("policy.modifiedby.com/inherit-env-label-from-namespace"),
value: "true"
}
]
:
[
JSONPatch{
op: "add",
path: "/metadata/annotations",
value: {"policy.modifiedby.com/inherit-env-label-from-namespace": "true"}
}
]
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: inherit-env-label-from-namespace-binding
spec:
policyName: inherit-env-label-from-namespace
matchResources:
namespaceSelector:
matchExpressions:
- { key: env, operator: In, values: [dev,ppr,prd] }
Creating the namespaces should generate errors, because some of those do not respect the requirements of the VAP
➜ ~ k apply -f ./04_ns.yaml
namespace/testmap1 created
namespace/testmap2 created
namespace/testmap5 created
Error from server (Invalid): error when creating "./04_ns.yaml": namespaces "testmap3" is forbidden: ValidatingAdmissionPolicy 'require-env-label-on-namespace' with binding 'require-env-label-on-namespace-binding' denied request: Namespace should have a valid 'env' defined (dev|prd|ppr|staging).
Error from server (Invalid): error when creating "./04_ns.yaml": namespaces "testmap4" is forbidden: ValidatingAdmissionPolicy 'require-env-label-on-namespace' with binding 'require-env-label-on-namespace-binding' denied request: Namespace should have a valid 'env' defined (dev|prd|ppr|staging).
Now we can try to create some pods in the namespaces that were allowed.
➜ ~ k get pod -n testmap3
No resources found in testmap3 namespace.
➜ ~ k run testmapdev1 -n testmap2 --image nginx
Warning: would violate PodSecurity "restricted:v1.36": allowPrivilegeEscalation != false (container "testmapdev1" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "testmapdev1" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "testmapdev1" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "testmapdev1" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
pod/testmapdev1 created
➜ ~ k run testmapppr1 -n testmap5 --image nginx
Warning: would violate PodSecurity "restricted:v1.36": allowPrivilegeEscalation != false (container "testmapppr1" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "testmapppr1" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "testmapppr1" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "testmapppr1" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
pod/testmapppr1 created
➜ ~ k run testmapprd1 -n testmap1 --image nginx
Error from server (Forbidden): pods "testmapprd1" is forbidden: violates PodSecurity "restricted:v1.36": allowPrivilegeEscalation != false (container "testmapprd1" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "testmapprd1" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "testmapprd1" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "testmapprd1" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
We can notice that the creation fails in the namespace with the env=prd, because of the enforce status of the PSS.
Also we can check the env label inheritance for the other pods.
➜ ~ k get pod -n testmap2 -o json |jq '.items[].metadata.labels'
{
"env": "dev",
"run": "testmapdev1"
}
➜ ~ k get pod -n testmap5 -o json |jq '.items[].metadata.labels'
{
"env": "ppr",
"run": "testmapppr1"
}
Ok, that’s nice, and we’ll wrap here for today.
4. Summary
So in this article, we moved from the ValildatingAdmissionPolicy to the MutatingAdmissionPolicy.
The principles are quite the same. First create a policy, then bind it with a policy binding.
Still using the CEL.
The main difference reside in what we want to achieve, which is classified as mutations.
Coming next is a usecase of a MAP to enforce the PSS with a MAP which help us to go deeper into the CEL syntax and the mutations.