Admission control Kubernetes pour les images de container : que vérifier à l'entrée.
Un validating admission webhook est le seul point par lequel passe chaque pod avant de tourner, quel que soit celui qui l'a appliqué. Ce guide dit ce que cette barrière devrait exiger d'une image de container, quoi faire quand elle n'a pas encore de réponse, et comment vérifier qu'elle tient vraiment.
Pourquoi l'entrée, pas le post-mortem
La sécurité runtime vous dit qu'un container s'est mis à se comporter bizarrement à trois heures du matin. L'admission control décide s'il a seulement le droit de démarrer. Les deux répondent à des questions différentes, et la seconde coûte moins cher à bien traiter : à l'admission, l'API server attend déjà un oui ou un non, la référence de l'image est là, dans le pod spec, et un refus coûte un déploiement raté plutôt qu'un incident.
Comme le contrôle se trouve dans la chaîne d'admission de l'API server, peu importe qui applique le manifeste (kubectl, Argo CD, Flux, un Job, un operator qui réconcilie sa propre CRD). Chaque CREATE et UPDATE de pod hors d'une courte liste de namespaces exemptés passe par le même webhook, et une synchronisation GitOps qui viole une policy échoue avec le message de refus comme erreur, exactement là où vous voulez le lire.
Les cinq contrôles
Voici les questions auxquelles une barrière d'images peut répondre à partir du pod spec et d'un cache de scan, dans l'ordre où les activer. Les trois premières ne demandent aucun scanner.
- Hygiène des tags. Un tag modifiable peut changer ce qui tourne sans que personne ne déploie. Refuser
:latestest la plus petite règle possible, et celle qu'il vaut le plus la peine d'avoir dès le premier jour. - Liste blanche de registries. Tout ce qui ne vient pas de vos registries est refusé d'emblée. Une liste vide autorise tous les registries ; une liste non vide refuse tous les autres.
- Épinglage par digest. Trois niveaux : autoriser les tags, préférer un digest, en exiger un. Exiger un digest est l'affirmation la plus forte sur ce qui a tourné et la plus contraignante à déployer ; la plupart des parcs commencent par « préférer » et resserrent namespace par namespace.
- Seuils de vulnérabilités. Refuser au moindre résultat critical, au moindre high, ou au-delà d'un plafond de mediums et de lows. C'est le contrôle qui demande un scanner derrière lui, et c'est là que la section suivante compte.
- Signatures et attestations. Exiger une signature cosign vérifiée, une attestation SBOM, une attestation de scan de vulnérabilités, et refuser les attestations plus anciennes qu'un âge maximal. Activez-les quand votre pipeline de build les produit vraiment ; avant, elles refusent tout.
Au-delà de l'image, le même examen peut regarder le workload : containers privilégiés, montages hostPath et apparentés sont des constats de durcissement, et une policy décide s'ils refusent, avertissent ou passent.
Une image encore jamais scannée
Un scan prend des minutes ; l'API server attend des secondes. Une barrière qui scanne en ligne finit donc par expirer et emporter les déploiements avec elle. La conception viable sépare les deux : un scanner remplit un cache en arrière-plan, et l'admission lit le cache. Un registry lent ne peut alors pas ralentir un déploiement.
La conséquence à trancher avant d'installer : un digest que personne n'a encore vu n'a pas encore de résultat de scan. Il y a deux réponses honnêtes, et dans Attestkeep le champ de policy admissionTiming en choisit une :
- Autoriser et scanner. Admettre l'image inconnue, mettre le scan en file immédiatement, et décider sur le rapport terminé à chaque admission ultérieure de ce digest. Facile à adopter, et c'est le comportement par défaut : rien de ce que vous déployez le premier jour n'est refusé parce que c'est nouveau.
- Refuser tant que non scannée. Refuser l'image inconnue avec « a scan has been queued — retry shortly », et l'admettre une fois le rapport disponible. Chaque déploiement d'un nouveau digest coûte une nouvelle tentative. Choisissez-le pour les namespaces qui le justifient.
Dans les deux cas, le premier scan d'un nouveau digest tourne en arrière-plan. Sur un cluster neuf, il peut prendre quelques minutes (le scanner récupère l'image et la base de vulnérabilités est froide), et tant qu'il n'est pas terminé, l'image est unknown, jamais compliant. Les deux réglages existent dans toutes les éditions d'Attestkeep, Community comprise.
Fail-open ou fail-closed
L'autre décision de posture ne concerne pas l'image mais la barrière elle-même. Que doit faire l'API server quand votre webhook ne répond pas ? Kubernetes appelle cela la failurePolicy du webhook, et aucune des deux valeurs n'est juste partout.
- Ignore (fail-open) garde le cluster déployable pendant une panne du webhook et laisse un trou : pendant cette fenêtre, rien n'a été examiné. Un contrôle digne de confiance enregistre cette fenêtre au lieu d'afficher en silence un trimestre propre.
- Fail (fail-closed) vous donne un contrôle dont on peut prouver qu'il n'a jamais eu de trou, et signifie qu'une panne du webhook arrête les déploiements. C'est un vrai coût, à choisir délibérément, avec le nombre de répliques et la supervision pour l'assumer.
Ce qui est passé pendant un trou tourne encore après. C'est un rapprochement périodique qui le trouve : comparer le digest d'image observé de chaque container en cours d'exécution avec le registre d'admission, et signaler ceux qui n'ont aucune décision derrière eux (ou les arrêter et les réadmettre). Attestkeep est livré en Ignore par défaut, enregistre chaque fenêtre de panne pour le document de preuves et lance ce rapprochement toutes les heures.
Prouver qu'elle tient
Un webhook d'admission qui ne tourne plus sans que personne ne le remarque ressemble exactement à un cluster conforme. La première chose à faire après l'installation d'une barrière, et après chaque mise à jour, est donc de la voir refuser quelque chose. Avec la policy par défaut d'Attestkeep, la plus petite violation est le tag :latest :
kubectl run enforcement-check --image=nginx:latest
Error from server: admission webhook "image-security.attestkeep.com" denied the request:
image security policy: nginx:latest: the :latest tag is blocked by policy
kubectl get pod enforcement-check
Error from server (NotFound): pods "enforcement-check" not foundDeux choses méritent l'attention. Le message commence par Error from server : le refus a eu lieu dans la chaîne d'admission de l'API server, pas dans un agent qui aurait pu dormir. Et la raison nomme la règle ; un développeur qui la lit dans un log de CI sait quoi corriger. Si le pod a été créé à la place, vérifiez les suspects habituels dans l'ordre : un namespace exempté, une policy en mode audit, un webhook qui laisse passer en fail-open, une installation jamais activée.
Garder la trace
Un refus qui ne laisse aucune trace, c'est un contrôle sans preuve. Enregistrez les autorisations autant que les refus (des preuves qui ne listent que des refus ne peuvent pas montrer qu'un contrôle fonctionnait), et enregistrez chaque décision sous le hash de la policy qui l'a prise : « quelle règle était en vigueur quand cela a été admis ? » devient alors une simple requête, et non une fouille archéologique dans l'historique git.
C'est la partie que les outils gratuits ne vous donnent pas, et celle qu'un auditeur lit vraiment. Attestkeep calcule le hash du contenu de chaque décision à l'écriture, scelle le registre sous des checkpoints signés et chaînés, et transforme les décisions d'une période en un paquet de preuves signé, rapporté au jeu de contrôles que vous choisissez. Comment toute altération de ce registre devient détectable, et où s'arrêtent exactement ses garanties, est expliqué sur la page evidence trust model (en anglais).
Chaque réglage cité ici est documenté : Policies pour la référence des champs et des exemples prêts à appliquer, Admission pour les deux réglages de posture, et Prove it is enforcing pour le runbook complet. L'édition Community est gratuite et contient chaque contrôle de cette page.