Aller au contenu
Conteneurs & Orchestration high

ValidatingAdmissionPolicy & MutatingAdmissionPolicy, Policies natives en CEL (CKS)

26 min de lecture

Logo Kubernetes

ValidatingAdmissionPolicy (VAP) est la solution native Kubernetes pour valider les ressources à l'admission sans webhook externe. Ce guide est orienté préparation CKS : il vous apprend à utiliser VAP, mais surtout à choisir le bon mécanisme (VAP, PSA, RBAC, webhook) et à diagnostiquer rapidement un refus.

  • Choisir entre VAP, Pod Security Admission, RBAC et webhooks
  • Créer des policies pour securityContext, hostPath et registries
  • Diagnostiquer rapidement un refus d'admission
  • Connaître les limites de VAP pour l'examen

VAP a atteint la maturité GA (General Availability) dans Kubernetes 1.30, ce qui signifie qu'il est stable et supporté pour la production. Depuis Kubernetes 1.36 (avril 2026), son pendant MutatingAdmissionPolicy est lui aussi GA et activé par défaut : Kubernetes dispose désormais d'un duo natif complet validation + mutation, sans webhook externe.

FeatureVersionÉtatNotes CKS
ValidatingAdmissionPolicy1.30+GAUtilisable à l'examen
MutatingAdmissionPolicy1.36+GA, activée par défautDisponible nativement

Question clé pour la CKS : quel mécanisme pour quel besoin ?

Kubernetes offre plusieurs mécanismes de contrôle. La confusion entre eux est fréquente, même chez les professionnels expérimentés. Ce tableau vous aide à choisir le bon outil selon votre objectif.

BesoinMécanisme recommandéPourquoi
Forcer runAsNonRoot, drop capabilitiesPod Security AdmissionConçu pour ça, 3 profils prêts à l'emploi
Empêcher un user de créer des SecretsRBACContrôle d'accès par identité
Bloquer hostPath ou registries non autoriséesVAPValidation de contenu, pas d'identité
Injecter un label par défaut, ajouter runAsNonRoot: trueMutatingAdmissionPolicy (GA en 1.36)Mutation déclarative en CEL, sans webhook
Muter avec génération d'autres ressources (NetworkPolicy, Secret)KyvernoSeul à proposer la génération
Exiger une signature d'imageImagePolicyWebhook / KyvernoVAP ne vérifie pas les signatures
Logique métier complexeWebhook externePlus de flexibilité que CEL

Règle pratique : Si le contrôle porte sur l'identité (qui fait l'action), c'est RBAC. Si le contrôle porte sur le contenu (ce qui est créé), c'est VAP ou PSA.

VAP intervient à un moment précis de la chaîne d'admission : après l'authentification et l'autorisation RBAC, sur le contenu de l'objet soumis. Tout ce qui se joue avant (qui parle) ou après (ce qui circule sur le réseau, ce qui est signé) lui échappe par construction. La liste ci-dessous n'est donc pas un inventaire de manques à combler : ce sont des couches complémentaires qui doivent coexister.

  • RBAC : VAP ne contrôle pas qui peut faire quoi, seulement quoi est créé
  • Pod Security Admission : pour les baselines de sécurité standard, PSA est plus simple
  • NetworkPolicy : VAP ne filtre pas le trafic réseau
  • Signature d'artefacts : VAP ne vérifie pas les signatures cosign/notation
  • Audit avancé : VAP ne génère pas de rapports de conformité détaillés

Une policy VAP se compose de trois ressources, et cette séparation est ce qui déroute au premier contact. La ValidatingAdmissionPolicy porte la logique en CEL mais ne s'applique à rien tant qu'elle est seule : déclarée sans binding, elle est inerte, c'est le piège le plus fréquent. Le ValidatingAdmissionPolicyBinding décide et comment elle agit (quels namespaces, en mode Audit, Warn ou Deny). La ressource de paramètres, optionnelle, est un ConfigMap ou une CRD qui rend la même logique réutilisable avec des valeurs différentes.

Architecture VAP : Policy, Binding et ConfigMap

Retenez la conséquence pratique : une policy et son binding sont deux objets kubectl apply distincts, et vous devez vérifier les deux quand une règle « ne fait rien ».

Avant VAP, pour valider des ressources au-delà de ce que PSA permet, il fallait déployer un webhook externe (Kyverno, Gatekeeper, ou un webhook maison). Cela implique de maintenir un composant supplémentaire, de gérer ses certificats TLS, et de surveiller sa disponibilité.

VAP élimine cette complexité opérationnelle pour les cas de validation simples.

AspectVAP + MAP (natifs)Webhooks (Kyverno, Gatekeeper)
InstallationRien à installerDéploiement requis
PerformanceIn-process, très rapideAppel réseau
DisponibilitéPas de dépendance réseau externeSPOF si mal configuré
LangageCEL uniquementYAML, Rego, CEL
MutationGA en 1.36 (MutatingAdmissionPolicy)Stable et mature
AuditvalidationActions: [Audit]Rapports détaillés

Quand choisir un webhook malgré tout ? Si vous avez besoin de génération de ressources (Kyverno crée des NetworkPolicies à partir d'un template) ou de rapports de conformité détaillés (PolicyReports), les webhooks restent nécessaires. La mutation simple (ajout de labels, valeurs par défaut) bascule désormais en natif via MutatingAdmissionPolicy.

Cet exemple est directement aligné avec l'objectif CKS "Use appropriate pod security standards" du domaine Minimize Microservice Vulnerabilities. Il impose plusieurs contraintes de sécurité :

  • runAsNonRoot: true
  • allowPrivilegeEscalation: false
  • readOnlyRootFilesystem: true
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-secure-securitycontext
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
# Tous les conteneurs doivent avoir runAsNonRoot
- expression: |
object.spec.containers.all(c,
has(c.securityContext) &&
has(c.securityContext.runAsNonRoot) &&
c.securityContext.runAsNonRoot == true
)
message: "Tous les conteneurs doivent avoir runAsNonRoot: true"
reason: Forbidden
# Interdire allowPrivilegeEscalation
- expression: |
object.spec.containers.all(c,
!has(c.securityContext) ||
!has(c.securityContext.allowPrivilegeEscalation) ||
c.securityContext.allowPrivilegeEscalation == false
)
message: "allowPrivilegeEscalation doit être false"
reason: Forbidden
# Exiger readOnlyRootFilesystem
- expression: |
object.spec.containers.all(c,
has(c.securityContext) &&
has(c.securityContext.readOnlyRootFilesystem) &&
c.securityContext.readOnlyRootFilesystem == true
)
message: "readOnlyRootFilesystem doit être true"
reason: Forbidden

Les volumes hostPath sont un vecteur d'attaque classique : ils permettent d'accéder au filesystem de l'hôte. Cette policy les interdit :

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deny-hostpath-volumes
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: |
!has(object.spec.volumes) ||
!object.spec.volumes.exists(v, has(v.hostPath))
message: "Les volumes hostPath sont interdits"
reason: Forbidden

Test :

Fenêtre de terminal
# Doit échouer
kubectl run hostpath-pod --image=nginx -n secure-ns \
--overrides='{
"spec": {
"volumes": [{"name": "host-vol", "hostPath": {"path": "/etc"}}],
"containers": [{
"name": "nginx",
"image": "nginx",
"volumeMounts": [{"name": "host-vol", "mountPath": "/host-etc"}],
"securityContext": {"runAsNonRoot": true, "runAsUser": 1000, "allowPrivilegeEscalation": false, "readOnlyRootFilesystem": true}
}]
}
}'

Exemple 3 : Limiter les registries autorisées (Supply Chain)

Section intitulée « Exemple 3 : Limiter les registries autorisées (Supply Chain) »

Cet exemple introduit le troisième composant de l'architecture : le paramétrage. La liste des registres autorisés ne vit pas dans la policy mais dans un ConfigMap, désigné par le paramRef du binding et lu en CEL via params. L'intérêt est direct : ajouter un registre se fait par un kubectl edit configmap, sans toucher à la logique de validation ni la redéployer. Notez le parameterNotFoundAction: Deny du binding, qui décide du comportement si le ConfigMap disparaît : refuser par défaut est le seul choix défendable sur une règle de sécurité, l'alternative laisserait tout passer silencieusement.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: restrict-image-registries
spec:
failurePolicy: Fail
paramKind:
apiVersion: v1
kind: ConfigMap
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
variables:
- name: allowedRegistries
expression: |
params.data.allowedRegistries.split(',')
validations:
- expression: |
object.spec.containers.all(c,
variables.allowedRegistries.exists(r, c.image.startsWith(r))
)
messageExpression: |
'Image non autorisée. Registries autorisés: ' + params.data.allowedRegistries
reason: Forbidden

Mutation native avec MutatingAdmissionPolicy (GA 1.36)

Section intitulée « Mutation native avec MutatingAdmissionPolicy (GA 1.36) »

Depuis Kubernetes 1.36, vous pouvez muter des ressources avec la même approche déclarative que VAP, mais via une MutatingAdmissionPolicy (MAP). Le pattern à retenir : VAP valide, MAP modifie, les deux s'écrivent en CEL et s'appliquent via un Binding.

Les mutations supportent deux patchType :

  • ApplyConfiguration, décrit l'objet partiel à fusionner via Server-Side Apply. Lisible, robuste sur les champs structurés.
  • JSONPatch, liste d'opérations JSON Patch (RFC 6902). Plus expressif mais plus fragile sur les chemins absents.

Cet exemple ajoute automatiquement le label team: platform à tout pod créé dans le namespace map-test. Pratique pour appliquer des conventions sans dépendre des développeurs.

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: add-team-label
spec:
failurePolicy: Fail
reinvocationPolicy: Never
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
matchConditions:
- name: only-test-namespace
expression: "request.namespace == 'map-test'"
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: |
Object{
metadata: Object.metadata{
labels: {"team": "platform"}
}
}

Quand préférer MAP plutôt qu'un webhook Kyverno ?

Section intitulée « Quand préférer MAP plutôt qu'un webhook Kyverno ? »

Le tableau se lit par ses deux lignes du milieu, qui portent toute la décision. Sur une mutation simple, les deux outils font la même chose et le natif gagne par élimination du composant à exploiter. Dès que le besoin devient génération ou vérification de signature, MAP ne peut pas répondre, la question ne se pose plus.

CritèreMAP (natif)Kyverno mutation
InstallationRien à déployerDéploiement requis (HA, certificats)
Mutation simple (label, default)IdéalIdéal
Génération de ressourcesNonOui (NetworkPolicy, Secret, ConfigMap)
Vérification d'imagesNonOui (Cosign/Sigstore)
LangageCELYAML + CEL

MAP couvre les mutations « valeurs par défaut » et « ajout de label ». Kyverno reste nécessaire pour la génération d'autres objets ou la vérification de signature d'images.

Lorsque vous écrivez une expression CEL dans une policy VAP, vous avez accès à plusieurs variables injectées automatiquement par Kubernetes. Comprendre ces variables est essentiel pour écrire des expressions efficaces.

VariableDescriptionUsage principal
objectRessource entrante (CREATE/UPDATE)Valider les champs
oldObjectRessource existante (UPDATE uniquement)Empêcher certaines modifications
requestMétadonnées (user, operation, namespace)Règles conditionnelles
paramsParamètres de la policy (ConfigMap/CRD)Rendre configurable
namespaceObjectLe namespace de la ressourceVérifier labels du namespace

Exemple pratique :

  • object.spec.containers : accéder aux conteneurs du pod entrant
  • oldObject.metadata.labels : comparer avec les labels actuels lors d'un UPDATE
  • request.userInfo.username : connaître qui fait la requête
  • params.data.maxReplicas : lire une valeur du ConfigMap paramétrique

Ces expressions couvrent l'essentiel de ce qu'on demande en épreuve pratique. Trois fonctions reviennent en permanence et il faut savoir laquelle choisir : all() vérifie que tous les éléments respectent une condition (utile pour imposer), exists() qu'au moins un la respecte (utile pour interdire, en préfixant d'un !), et has() teste la présence d'un champ avant d'y accéder. Le réflexe à ancrer : has() ne protège que d'un niveau. Écrire has(c.securityContext.runAsNonRoot) sur un conteneur dépourvu de securityContext déclenche une erreur d'évaluation, et avec failurePolicy: Fail la requête est refusée avec un message illisible qui recopie l'expression entière. D'où le double has() dans les exemples.

# Exiger runAsNonRoot
object.spec.containers.all(c,
has(c.securityContext) &&
has(c.securityContext.runAsNonRoot) &&
c.securityContext.runAsNonRoot == true
)
# Refuser privileged (le premier has() évite l'erreur si securityContext est absent)
!object.spec.containers.exists(c,
has(c.securityContext) &&
has(c.securityContext.privileged) &&
c.securityContext.privileged == true
)
# Refuser hostPath (le has() couvre le cas d'un pod sans volume)
!has(object.spec.volumes) ||
!object.spec.volumes.exists(v, has(v.hostPath))
# Refuser hostNetwork/hostPID/hostIPC
!object.spec.hostNetwork &&
!object.spec.hostPID &&
!object.spec.hostIPC
# Vérifier préfixe d'image
object.spec.containers.all(c, c.image.startsWith('gcr.io/'))
# Exclure les namespaces système
!request.namespace.startsWith("kube-")

Une erreur courante est de déployer une policy directement en mode Deny et de casser la production. Les actions de validation permettent un déploiement progressif et sécurisé.

ActionEffetPhase
AuditLog seulement, accepte la requêteDécouverte
WarnAccepte + warning visible dans kubectlTransition
DenyRefuse la requêteProduction

Pourquoi cette progression ?

  1. Audit : Vous déployez la policy sans impact. Les violations sont loggées, vous découvrez quelles ressources existantes ne seraient pas conformes.
  2. Warn : Les développeurs voient les warnings lors de leurs déploiements. Ils ont le temps de corriger avant le blocage.
  3. Deny : Une fois tous les workloads conformes, vous activez le blocage.

Stratégie recommandée :

# Étape 1 : Observer (pas d'impact)
validationActions: [Audit]
# Étape 2 : Alerter (les users voient les warnings)
validationActions: [Warn, Audit]
# Étape 3 : Bloquer
validationActions: [Deny]

Ces cinq commandes se lancent dans l'ordre et éliminent une hypothèse chacune : la policy existe-t-elle, son expression compile-t-elle, le refus a-t-il laissé une trace, que voit exactement le client, le namespace porte-t-il le label attendu par le binding. La troisième mérite un avertissement : un refus VAP sur une création directe (kubectl run, kubectl apply) ne produit aucun Event, l'erreur est simplement renvoyée à l'appelant. Vous ne verrez des Events que lorsqu'un contrôleur retente la création, typiquement un ReplicaSet qui remonte des FailedCreate contenant le nom de la policy.

Fenêtre de terminal
# 1. Lister toutes les policies et bindings
kubectl get validatingadmissionpolicies,validatingadmissionpolicybindings
# 2. Voir les détails d'une policy (erreurs de type-checking)
kubectl describe validatingadmissionpolicy <name>
# 3. Voir les events récents (uniquement si un contrôleur retente la création)
kubectl get events -A --sort-by=.lastTimestamp | grep -i admission
# 4. Tester un manifest avec verbosité
kubectl apply -f pod.yaml --v=8
# 5. Vérifier si le namespace a les bons labels
kubectl get namespace <ns> --show-labels

Un refus à la création peut venir de plusieurs sources. C'est une source fréquente de confusion : vous pensiez que VAP bloquait, mais c'était PSA. Ou l'inverse. Voici comment identifier la source exacte.

SourceComment vérifierMessage typique
ValidatingAdmissionPolicykubectl get validatingadmissionpoliciesValidatingAdmissionPolicy 'xxx' with binding 'yyy' denied request
Pod Security AdmissionLabel pod-security.kubernetes.io/*violates PodSecurity "restricted"
ResourceQuotakubectl describe quotaexceeded quota
LimitRangekubectl describe limitrangemust be less than or equal to
RBACkubectl auth can-iforbidden: User "x" cannot create
Webhook externekubectl get validatingwebhookconfigurationsadmission webhook "xxx" denied

Astuce de diagnostic rapide : Le message d'erreur contient généralement le nom du mécanisme qui bloque. Lisez-le attentivement avant de chercher plus loin.

Ces erreurs apparaissent lors du type-checking de la policy ou à l'exécution. Elles sont souvent dues à des champs optionnels non testés.

ErreurCauseSolution
undefined field 'xxx'Champ inexistantUtiliser has(object.field)
params missingConfigMap absentCréer le ConfigMap
type mismatchMauvais type (string vs int)Vérifier le schema

Exemple de type-checking dans le status :

status:
typeChecking:
expressionWarnings:
- fieldRef: spec.validations[0].expression
warning: |-
ERROR: undefined field 'replicas'

L'épreuve CKS est chronométrée et ne récompense pas l'écriture de policies sophistiquées. Sur les six réflexes ci-dessous, les points 3 et 4 sont ceux qui font perdre le plus de temps quand ils manquent : chercher un binding à la main ou confondre un refus PSA avec un refus VAP coûte plusieurs minutes sur une question qui en vaut peu. Entraînez-les en priorité.

  1. Reconnaître quand une admission policy est le bon levier (vs PSA, RBAC)
  2. Lire rapidement une ValidatingAdmissionPolicy existante
  3. Trouver le binding qui applique une policy
  4. Identifier la source d'un refus (VAP, PSA, quota, webhook...)
  5. Corriger une expression CEL simple (has(), exists(), all())
  6. Créer une policy basique en moins de 5 minutes

Ce squelette contient le strict nécessaire pour qu'une policy s'applique réellement, et rien d'autre. Deux points à ne pas oublier sous pression : le policyName du binding doit correspondre exactement au nom de la policy, une faute de frappe ne produit aucune erreur au kubectl apply et la règle reste sans effet ; et sans validationActions: [Deny], le binding se contente d'auditer. Le namespaceSelector suppose que le namespace cible porte bien le label, à vérifier avec kubectl get ns --show-labels.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: my-policy
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
validations:
- expression: "!object.spec.hostNetwork"
message: "hostNetwork interdit"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: my-policy-binding
spec:
policyName: my-policy
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels:
enforce-policy: "true"

Les matchConditions s'évaluent avant les validations et servent à écarter des requêtes que la policy ne doit jamais examiner. La distinction avec matchConstraints compte : ce dernier filtre par type de ressource, les matchConditions filtrent par contenu de la requête en CEL, avec accès à request et donc à l'utilisateur appelant. C'est le mécanisme qui évite le scénario classique du cluster bloqué : une policy trop large qui refuse les Pods système empêche le cluster de se réparer, et vous ne pouvez plus déployer le correctif. Excluez systématiquement les namespaces kube-* et les comptes system: avant de passer en Deny.

spec:
matchConditions:
- name: exclude-system-namespaces
expression: '!request.namespace.startsWith("kube-")'
- name: exclude-system-users
expression: '!request.userInfo.username.startsWith("system:")'
  1. VAP est GA depuis 1.30, MAP est GA depuis 1.36, Kubernetes a désormais un duo natif validation + mutation, sans webhook
  2. VAP ne remplace pas PSA, RBAC ou NetworkPolicy, choisir le bon outil
  3. 3 ressources : Policy + Binding + Paramètre (optionnel)
  4. Variables CEL : object, oldObject, request, params
  5. Diagnostic : kubectl get validatingadmissionpolicies,validatingadmissionpolicybindings puis describe (ces ressources n'ont aucun nom court, kubectl get vap renvoie une erreur)
  6. Supply Chain : VAP peut restreindre les registries, mais ne vérifie pas les signatures
  7. Pour la CKS : savoir créer une policy simple ET identifier la source d'un refus

Le quiz porte sur les arbitrages du guide, pas sur la mémorisation de la syntaxe CEL : distinguer VAP de PSA et de RBAC, savoir quelle ressource manque quand une policy reste sans effet, identifier la source d'un refus.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
8 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn