
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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
Versions et disponibilité
Section intitulée « Versions et disponibilité »VAP a atteint la maturité GA (General Availability) dans Kubernetes 1.30, ce qui signifie qu’il est stable et supporté pour la production. La mutation native (MutatingAdmissionPolicy) suit un chemin différent et n’est pas encore prête.
| Feature | Version | État | Notes CKS |
|---|---|---|---|
| ValidatingAdmissionPolicy | 1.30+ | GA | ✅ Utilisable à l’examen |
| MutatingAdmissionPolicy | 1.34 | Beta, off par défaut | ⚠️ Nécessite feature gate |
Quand utiliser VAP vs les alternatives
Section intitulée « Quand utiliser VAP vs les alternatives »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.
| Besoin | Mécanisme recommandé | Pourquoi |
|---|---|---|
| Forcer runAsNonRoot, drop capabilities | Pod Security Admission | Conçu pour ça, 3 profils prêts à l’emploi |
| Empêcher un user de créer des Secrets | RBAC | Contrôle d’accès par identité |
| Bloquer hostPath ou registries non autorisées | VAP | Validation de contenu, pas d’identité |
| Muter les ressources (ajouter labels, sidecars) | Kyverno / Gatekeeper | VAP ne mute pas (ou pas encore stable) |
| Exiger une signature d’image | ImagePolicyWebhook / Kyverno | VAP ne vérifie pas les signatures |
| Logique métier complexe | Webhook externe | Plus 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.
Ce que VAP ne remplace PAS
Section intitulée « Ce que VAP ne remplace PAS »- 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
Architecture VAP
Section intitulée « Architecture VAP »Une policy VAP se compose de trois ressources :
Pourquoi VAP plutôt qu’un webhook externe ?
Section intitulée « Pourquoi VAP plutôt qu’un webhook externe ? »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.
| Aspect | VAP (natif) | Webhooks (Kyverno, Gatekeeper) |
|---|---|---|
| Installation | Rien à installer | Déploiement requis |
| Performance | In-process, très rapide | Appel réseau |
| Disponibilité | Pas de dépendance réseau externe | SPOF si mal configuré |
| Langage | CEL uniquement | YAML, Rego, CEL |
| Mutation | Beta 1.34, off par défaut | ✅ Stable et mature |
| Audit | validationActions: [Audit] | Rapports détaillés |
Quand choisir un webhook malgré tout ? Si vous avez besoin de mutation avancée, de génération de ressources, ou de rapports de conformité détaillés, les webhooks externes restent nécessaires.
Exemple 1 : Imposer un securityContext sécurisé
Section intitulée « Exemple 1 : Imposer un securityContext sécurisé »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: trueallowPrivilegeEscalation: falsereadOnlyRootFilesystem: true
apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicymetadata: name: require-secure-securitycontextspec: 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.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: ForbiddenapiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicyBindingmetadata: name: require-secure-securitycontext-bindingspec: policyName: require-secure-securitycontext validationActions: [Deny] matchResources: namespaceSelector: matchLabels: security: strict# Créer le namespace avec le labelkubectl create namespace secure-nskubectl label namespace secure-ns security=strict
# Appliquer la policy et le bindingkubectl apply -f secure-securitycontext-policy.yamlkubectl apply -f secure-securitycontext-binding.yaml
# Test ÉCHEC : pod sans runAsNonRootkubectl run bad-pod --image=nginx -n secure-ns
# Test SUCCÈS : pod avec securityContext completkubectl run good-pod --image=nginx -n secure-ns \ --overrides='{ "spec": { "containers": [{ "name": "nginx", "image": "nginx", "securityContext": { "runAsNonRoot": true, "runAsUser": 1000, "allowPrivilegeEscalation": false, "readOnlyRootFilesystem": true } }] } }'Exemple 2 : Refuser les volumes hostPath
Section intitulée « Exemple 2 : Refuser les volumes hostPath »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/v1kind: ValidatingAdmissionPolicymetadata: name: deny-hostpath-volumesspec: 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: ForbiddenTest :
# Doit échouerkubectl 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) »apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicymetadata: name: restrict-image-registriesspec: 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: ForbiddenapiVersion: v1kind: ConfigMapmetadata: name: allowed-registries namespace: secure-nsdata: allowedRegistries: "gcr.io/my-project,docker.io/library"apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicyBindingmetadata: name: restrict-registries-bindingspec: policyName: restrict-image-registries validationActions: [Deny] paramRef: name: allowed-registries namespace: secure-ns parameterNotFoundAction: Deny matchResources: namespaceSelector: matchLabels: security: strictVariables CEL disponibles
Section intitulée « Variables CEL disponibles »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.
| Variable | Description | Usage principal |
|---|---|---|
object | Ressource entrante (CREATE/UPDATE) | Valider les champs |
oldObject | Ressource existante (UPDATE uniquement) | Empêcher certaines modifications |
request | Métadonnées (user, operation, namespace) | Règles conditionnelles |
params | Paramètres de la policy (ConfigMap/CRD) | Rendre configurable |
namespaceObject | Le namespace de la ressource | Vérifier labels du namespace |
Exemple pratique :
object.spec.containers: accéder aux conteneurs du pod entrantoldObject.metadata.labels: comparer avec les labels actuels lors d’un UPDATErequest.userInfo.username: connaître qui fait la requêteparams.data.maxReplicas: lire une valeur du ConfigMap paramétrique
Expressions CEL courantes pour la CKS
Section intitulée « Expressions CEL courantes pour la CKS »# Exiger runAsNonRootobject.spec.containers.all(c, has(c.securityContext.runAsNonRoot) && c.securityContext.runAsNonRoot == true)
# Refuser privileged!object.spec.containers.exists(c, has(c.securityContext.privileged) && c.securityContext.privileged == true)
# Refuser hostPath!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'imageobject.spec.containers.all(c, c.image.startsWith('gcr.io/'))
# Exclure les namespaces système!request.namespace.startsWith("kube-")Actions de validation et déploiement progressif
Section intitulée « Actions de validation et déploiement progressif »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é.
| Action | Effet | Phase |
|---|---|---|
Audit | Log seulement, accepte la requête | Découverte |
Warn | Accepte + warning visible dans kubectl | Transition |
Deny | Refuse la requête | Production |
Pourquoi cette progression ?
- Audit : Vous déployez la policy sans impact. Les violations sont loggées, vous découvrez quelles ressources existantes ne seraient pas conformes.
- Warn : Les développeurs voient les warnings lors de leurs déploiements. Ils ont le temps de corriger avant le blocage.
- 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 : BloquervalidationActions: [Deny]Troubleshooting rapide (réflexes CKS)
Section intitulée « Troubleshooting rapide (réflexes CKS) »Commandes de diagnostic
Section intitulée « Commandes de diagnostic »# 1. Lister toutes les policies et bindingskubectl 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 (refus d'admission)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 labelskubectl get namespace <ns> --show-labelsD’où vient le refus ?
Section intitulée « D’où vient le refus ? »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.
| Source | Comment vérifier | Message typique |
|---|---|---|
| ValidatingAdmissionPolicy | kubectl get vap | ValidatingAdmissionPolicy 'xxx' denied |
| Pod Security Admission | Label pod-security.kubernetes.io/* | violates PodSecurity "restricted" |
| ResourceQuota | kubectl describe quota | exceeded quota |
| LimitRange | kubectl describe limitrange | must be less than or equal to |
| RBAC | kubectl auth can-i | forbidden: User "x" cannot create |
| Webhook externe | kubectl get validatingwebhookconfigurations | admission 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.
Erreurs CEL courantes
Section intitulée « Erreurs CEL courantes »Ces erreurs apparaissent lors du type-checking de la policy ou à l’exécution. Elles sont souvent dues à des champs optionnels non testés.
| Erreur | Cause | Solution |
|---|---|---|
undefined field 'xxx' | Champ inexistant | Utiliser has(object.field) |
params missing | ConfigMap absent | Créer le ConfigMap |
type mismatch | Mauvais 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'Ce qu’il faut savoir faire vite à l’examen
Section intitulée « Ce qu’il faut savoir faire vite à l’examen »- Reconnaître quand une admission policy est le bon levier (vs PSA, RBAC)
- Lire rapidement une ValidatingAdmissionPolicy existante
- Trouver le binding qui applique une policy
- Identifier la source d’un refus (VAP, PSA, quota, webhook…)
- Corriger une expression CEL simple (
has(),exists(),all()) - Créer une policy basique en moins de 5 minutes
Template minimal à mémoriser
Section intitulée « Template minimal à mémoriser »apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicymetadata: name: my-policyspec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] validations: - expression: "!object.spec.hostNetwork" message: "hostNetwork interdit"---apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicyBindingmetadata: name: my-policy-bindingspec: policyName: my-policy validationActions: [Deny] matchResources: namespaceSelector: matchLabels: enforce-policy: "true"Match conditions avancées
Section intitulée « Match conditions avancées »Pour exclure les namespaces système ou certains users :
spec: matchConditions: - name: exclude-system-namespaces expression: '!request.namespace.startsWith("kube-")' - name: exclude-system-users expression: '!request.userInfo.username.startsWith("system:")'À retenir
Section intitulée « À retenir »- VAP est GA depuis 1.30 — la mutation (MutatingAdmissionPolicy) est beta et off par défaut
- VAP ne remplace pas PSA, RBAC ou NetworkPolicy — choisir le bon outil
- 3 ressources : Policy + Binding + Paramètre (optionnel)
- Variables CEL :
object,oldObject,request,params - Diagnostic :
kubectl get vap,vapbindingpuisdescribepour les erreurs - Supply Chain : VAP peut restreindre les registries, mais ne vérifie pas les signatures
- Pour la CKS : savoir créer une policy simple ET identifier la source d’un refus