Aller au contenu
Conteneurs & Orchestration high
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

ValidatingAdmissionPolicy — Policies natives Kubernetes en CEL (CKS)

17 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. La mutation native (MutatingAdmissionPolicy) suit un chemin différent et n’est pas encore prête.

FeatureVersionÉtatNotes CKS
ValidatingAdmissionPolicy1.30+GA✅ Utilisable à l’examen
MutatingAdmissionPolicy1.34Beta, off par défaut⚠️ Nécessite feature gate

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é
Muter les ressources (ajouter labels, sidecars)Kyverno / GatekeeperVAP ne mute pas (ou pas encore stable)
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.

  • 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 :

Architecture VAP : Policy, Binding et ConfigMap

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 (natif)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
MutationBeta 1.34, off par défaut✅ Stable et mature
AuditvalidationActions: [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.

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.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) »
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

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
# Exiger runAsNonRoot
object.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'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]
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 (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 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 vapValidatingAdmissionPolicy 'xxx' denied
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'
  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
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"

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:")'
  1. VAP est GA depuis 1.30 — la mutation (MutatingAdmissionPolicy) est beta et off par défaut
  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 vap,vapbinding puis describe pour les erreurs
  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

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn