PodSecurityPolicy, l’ancien mécanisme de restriction des pods (interdire les conteneurs privilégiés, forcer un utilisateur non-root), a été retiré définitivement de Kubernetes en version 1.25, après plusieurs années de dépréciation. Pod Security Admission le remplace par un mécanisme radicalement plus simple : un label posé sur un namespace, sans CRD ni webhook externe à déployer.
Trois profils, une hiérarchie stricte
Pod Security Standards définit trois profils de restriction croissante. privileged n’impose aucune restriction, l’équivalent de l’absence de contrôle. baseline bloque les escalades de privilèges les plus évidentes (conteneurs privilégiés, accès direct au réseau ou au PID de l’hôte) sans gêner la grande majorité des workloads standards. restricted va beaucoup plus loin : utilisateur non-root obligatoire, profil seccomp explicitement défini, capacités Linux réduites au strict minimum.
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
Ce label suffit : aucun contrôleur supplémentaire à installer, le serveur API applique la restriction nativement dès qu’un pod est créé dans ce namespace.
Trois modes, pour migrer sans tout casser d’un coup
Chaque profil peut s’appliquer selon trois modes distincts et cumulables : enforce rejette réellement les pods non conformes, audit journalise une violation sans bloquer, warn affiche un avertissement côté client kubectl sans bloquer non plus. Cette distinction permet une migration progressive : passer un namespace en audit: restricted révèle, sans aucun risque de casse, tous les pods qui échoueraient si enforce était activé.
metadata:
labels:
# Observer l'impact réel avant d'imposer quoi que ce soit
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
# enforce reste absent : rien n'est encore bloqué
# Les violations en mode audit apparaissent dans les logs
# de l'API server, sans jamais bloquer un déploiement
kubectl logs -n kube-system -l component=kube-apiserver | grep "would violate"
Ce que restricted casse le plus souvent
Le profil restricted exige runAsNonRoot: true, ce qui échoue immédiatement sur toute image construite sans utilisateur non-root explicite, y compris de nombreuses images de base historiques qui tournent encore en root par défaut. Il exige aussi capabilities.drop: ["ALL"] et, sous Linux, un seccompProfile.type explicitement positionné à RuntimeDefault ou Localhost — l’absence de profil est refusée au même titre que Unconfined. C’est ce dernier contrôle qui surprend : un manifeste parfaitement fonctionnel sous baseline est rejeté sous restricted pour un champ que personne n’avait jamais eu besoin d’écrire.
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
seccompProfile:
# Absent, ce champ suffit à faire rejeter le pod sous restricted
type: RuntimeDefault
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true est souvent cité dans la foulée. C’est une bonne pratique de durcissement, mais elle ne fait partie d’aucun des trois profils : aucun contrôle de Pod Security Admission ne la vérifie, et un pod qui écrit sur son système de fichiers racine passe restricted sans broncher. Durcir ses images sur ce point est utile ; le faire en croyant préparer un passage à restricted revient à travailler sur le mauvais contrôle et à découvrir seccompProfile le jour de la bascule.
Ces exigences ne sont pas arbitraires : chacune ferme une voie d’escalade de privilèges documentée, mais elles supposent des images construites en conséquence, un prérequis souvent négligé jusqu’au moment où restricted est activé sur un namespace qui héberge des images plus anciennes.
Deux pièges du mécanisme lui-même
Le premier se paie en temps de diagnostic, parce qu’il n’échoue nulle part où l’on pense à regarder. enforce ne s’applique pas aux ressources de workload. La documentation le dit en une phrase : les modes audit et warn sont appliqués aux ressources de workload, « mais le mode enforce n’est pas appliqué aux ressources de workload, uniquement aux objets pod résultants ». Un kubectl apply d’un Deployment non conforme réussit donc, code de retour 0, message deployment.apps/... created. Le Deployment reste ensuite à 0/3, et le rejet existe bien, mais deux niveaux plus bas que la commande qui vient de rendre 0.
# La commande qui a "marché"
kubectl apply -f deployment.yaml
# deployment.apps/api created (code retour 0)
kubectl get deploy api
# READY 0/3
# Le rejet est ici, pas dans la sortie ci-dessus
kubectl describe rs -l app=api
kubectl get events --field-selector reason=FailedCreate
C’est la vraie raison de poser warn en même temps qu’enforce, et pas seulement avant : warn, lui, s’applique à la ressource de workload, donc l’avertissement remonte dans le terminal au moment du kubectl apply, là où le rejet d’enforce ne remontera jamais.
Le second piège est un défaut de plancher de version. Le label pod-security.kubernetes.io/<MODE>-version est optionnel : il épingle la politique à la version livrée avec une version mineure donnée de Kubernetes. Absent, c’est la valeur par défaut du contrôleur d’admission qui s’applique, et cette valeur par défaut documentée est latest. Un namespace étiqueté enforce: restricted sans enforce-version suit donc la définition de restricted de la version courante du cluster : la politique se durcit toute seule à la montée de version, sans qu’aucun manifeste n’ait bougé.
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
# Sans cette ligne, la politique suit le cluster.
# Avec, elle reste celle d'une version mineure connue.
pod-security.kubernetes.io/enforce-version: v1.37
Ce que le label ne protège pas
Pod Security Admission traite une seule question : ce qu’un pod a le droit de demander à son runtime. Le namespace qui porte le label n’est pas pour autant une frontière de sécurité, et la documentation Kubernetes sur le multi-tenant le formule sans ambiguïté — le modèle d’isolation par namespace « exige la configuration de plusieurs autres ressources Kubernetes, de plugins réseau, et le respect des bonnes pratiques de sécurité » pour isoler correctement des workloads.
Cinq mécanismes distincts, dont aucun n’est couvert par le label :
- Le réseau. Par défaut, tous les pods d’un cluster sont autorisés à communiquer entre eux et tout le trafic circule en clair ; le service DNS du cluster autorise par défaut les résolutions à travers tous les namespaces. Il faut une NetworkPolicy, et un plugin CNI qui l’implémente — sans quoi la ressource est ignorée sans erreur. Même correctement posée, elle laisse passer le tunnel de
kubectl port-forward, qui n’emprunte pas le chemin réseau qu’elle contrôle. - L’identité. Chaque namespace reçoit un ServiceAccount
defaultà sa création, un pod qui n’en désigne aucun l’obtient, et le montage de ses credentials d’API est le comportement par défaut : on s’en retire explicitement avecautomountServiceAccountToken: false. Ce que ce token permet ensuite relève de RBAC, pas du profil de pod. - Les ressources. Rien dans les trois profils ne borne la consommation CPU ou mémoire d’un conteneur. C’est le rôle de ResourceQuota et LimitRange.
- Le noyau et les nœuds. Les profils réduisent la surface d’attaque d’un conteneur, ils ne le sortent pas du noyau partagé. Même en dédiant des nœuds à un tenant, le kubelet et l’API server restent des services partagés, et la documentation avertit qu’un attaquant sorti d’un conteneur peut se déplacer latéralement dans le cluster. Les réponses à ce niveau sont le sandboxing, seccomp, AppArmor ou SELinux.
- Ce qui n’est pas namespacé. L’isolation par namespace « ne s’applique pas aux ressources Kubernetes qui ne peuvent pas être namespacées, comme les CustomResourceDefinitions, les StorageClasses et les Webhooks ». Un tenant qui peut créer une CRD la crée pour tout le cluster : c’est le point de bascule vers un plan de contrôle virtuel ou des clusters séparés.
Le label reste une des meilleures affaires du cluster : une ligne de YAML pour un contrôle natif, sans rien à installer. Il ne devient trompeur qu’au moment où on le prend pour la réponse plutôt que pour la première des cinq.
À retenir
Pod Security Admission remplace PodSecurityPolicy par un mécanisme natif et radicalement plus simple : un label de namespace, trois profils (privileged, baseline, restricted), trois modes (enforce, audit, warn) qui permettent d’observer l’impact avant de bloquer quoi que ce soit. Le profil restricted impose des contraintes réelles (non-root, seccomp explicite, capacités supprimées) qui échouent souvent sur des images plus anciennes, ce qui rend le mode audit préalable indispensable avant tout enforce en production, un des réflexes de durcissement qui accompagnent une migration Kubernetes sérieuse.