Pod Security et isolation
Comment un pod Kubernetes isole réellement ses processus du nœud hôte, et comment verrouiller cette isolation avec les security contexts, les capabilities Linux, seccomp et Pod Security Admission.
Table des matières
Pourquoi l'isolation d'un pod ne va pas de soi
Un conteneur n'est pas une machine virtuelle. Une VM embarque son propre noyau, isolé par un hyperviseur au niveau matériel. Un conteneur, lui, n'est qu'un processus Linux ordinaire auquel on a appliqué des restrictions de vue et de privilèges — il partage le même noyau que tous les autres conteneurs du nœud, et que le nœud lui-même. Cette différence structurelle a une conséquence directe : la frontière de sécurité d'un pod n'est pas garantie par construction, elle dépend entièrement de la configuration que vous lui appliquez.
Si un attaquant obtient l'exécution de code dans un conteneur mal configuré — root, toutes les capabilities, pas de filtrage syscall — il n'a pas seulement compromis une application. Il dispose potentiellement d'un accès proche de celui d'un processus root sur le nœud hôte, avec toutes les portes de sortie que cela implique : montage de volumes hôte, lecture de secrets d'autres pods via /proc, voire évasion complète du conteneur selon les vulnérabilités du runtime.
Le
securityContextn'est pas une option de durcissement facultative réservée aux environnements sensibles. C'est la seule couche qui transforme un processus Linux nu en conteneur réellement isolé. Un pod sanssecurityContextexplicite hérite des valeurs par défaut du runtime, qui sont historiquement permissives.
Ce chapitre part du noyau et remonte jusqu'à la politique de cluster : namespaces Linux, security context, capabilities, seccomp, puis Pod Security Admission comme mécanisme d'application à grande échelle.
Les namespaces Linux : la base de l'isolation
Kubernetes ne réinvente pas l'isolation, il orchestre des primitives du noyau Linux. Chaque conteneur — et par extension chaque pod — s'exécute dans un ensemble de namespaces, chacun limitant la visibilité d'une ressource système précise :
- PID — le conteneur ne voit que son propre arbre de processus, pas ceux du nœud ni des autres pods.
- NET — pile réseau dédiée (interfaces, routes, portsportRéseauxNuméro sur 16 bits qui désigne l'application destinataire sur une machine. Les ports 0 à 1023 sont réservés aux services système, comme 443 pour HTTPS.Voir dans le glossaire). Dans un pod multi-conteneurs, ce namespace est partagé entre tous les conteneurs du pod.
- MNT — vue du système de fichiers propre au conteneur.
- UTS — hostname et nom de domaine indépendants.
- IPC — segments de mémoire partagée et files de messages isolés.
- USER — fait correspondre l'UID 0 (root) vu à l'intérieur du conteneur à un UID non privilégié côté hôte. Disponible depuis Kubernetes 1.25 en alpha puis stabilisé progressivement ; à vérifier sur votre distribution avant d'en dépendre en production.
- CGROUP — limite la visibilité de la hiérarchie de cgroups, complément aux quotas CPU/mémoire eux-mêmes gérés par les cgroups.
Le partage du namespace réseau entre conteneurs d'un même pod signifie qu'ils se partagent aussi l'espace des ports. Deux conteneurs du même pod ne peuvent pas écouter sur le même port — un détail qui surprend souvent lors de la conception de patterns sidecar.
Ces namespaces isolent la vue, pas les privilèges. Un processus root dans son PID namespace reste root au sens des capabilities Linux, sauf si un USER namespace remappe explicitement son UID. C'est précisément l'angle mort que les couches suivantes viennent combler.
Security Context : verrouiller le comportement du conteneur
Le securityContext se déclare à deux niveaux dans un manifeste de pod : au niveau pod.spec.securityContext (valeurs par défaut pour tous les conteneurs) et au niveau container.securityContext (surcharge par conteneur, prioritaire).
Les champs qui comptent réellement en production :
| Champ | Effet | Recommandation |
|---|---|---|
runAsNonRoot |
Le kubelet refuse de démarrer le conteneur si l'utilisateur effectif est root | true systématiquement |
runAsUser / runAsGroup |
Fixe l'UID/GID d'exécution | UID fixe non nul, ex. 10001 |
allowPrivilegeEscalation |
Empêche setuid/setgid d'élever les privilèges pendant l'exécution |
false |
readOnlyRootFilesystem |
Rootfs monté en lecture seule | true + emptyDir pour les chemins réellement inscriptibles |
privileged |
Désactive quasiment toute isolation (accès aux devices du nœud) | false, sans exception hors cas d'infrastructure justifiés |
fsGroup |
GID appliqué aux volumes montés | à définir si l'image tourne avec un UID non root et doit écrire sur un volume |
runAsNonRoot: true seul ne fixe pas d'UID précis — il vérifie seulement que l'UID final n'est pas 0. Combinez-le toujours avec runAsUser pour un comportement déterministe, reproductible entre environnements.
Un piège classique : runAsNonRoot: true sur une image dont le Dockerfile ne définit aucun utilisateur non root fait échouer le démarrage du pod (CreateContainerConfigError). C'est le comportement voulu — mieux vaut un pod qui ne démarre pas qu'un pod qui tourne en root — mais cela signifie que la sécurité du security context est plafonnée par ce que l'image autorise. Un runAsUser: 10001 imposé sur une image qui écrit en dur dans /var/lib/app appartenant à root cassera l'application au runtime, pas à l'admission.
spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 containers: - name: api securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] add: ["NET_BIND_SERVICE"] volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}
Capabilities Linux : réduire les privilèges au strict nécessaire
Avant les capabilities, Linux ne connaissait que deux états : root (tout-puissant) ou utilisateur standard. Les capabilities découpent les privilèges root en une quarantaine de droits granulaires — CHOWN, NET_BIND_SERVICE, SYS_ADMIN, SYS_PTRACE, NET_RAW, etc. Un conteneur peut ainsi disposer d'exactement le privilège dont il a besoin, sans hériter du reste.
Par défaut, les runtimes de conteneurs (containerd, CRI-O) accordent un jeu d'une douzaine de capabilities jugées à faible risque — largement suffisant pour la plupart des workloads applicatifs, mais toujours supérieur au strict nécessaire pour un service HTTP qui ne fait qu'écouter un port et écrire des logs.
La pratique de durcissement standard consiste à tout retirer puis ne rajouter que ce qui est prouvé nécessaire :
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
NET_BIND_SERVICE est l'exemple canonique : sans elle, seul root peut ouvrir un port inférieur à 1024. Plutôt que de faire tourner tout le conteneur en root pour ce seul besoin, on accorde uniquement cette capability à un utilisateur non privilégié.
Identifier les capabilities réellement utilisées par une application legacy sans documentation demande souvent un audit dynamique — strace en environnement de test, ou l'observation des échecs EPERM après un drop: ["ALL"] en pré-production. Ne devinez pas, mesurez.
seccomp : filtrer les appels système
Les capabilities régulent quels privilèges un processus possède ; seccomp (secure computing mode) régule quels appels système il a le droit d'invoquer, indépendamment de ses privilèges. C'est une couche de filtrage au niveau du noyau, basée sur BPF, qui bloque des syscalls jugés dangereux ou inutiles pour un conteneur applicatif — ptrace, mount, reboot, kexec_load, add_key, entre autres.
Kubernetes expose seccomp via securityContext.seccompProfile :
RuntimeDefault— applique le profil par défaut du runtime de conteneurs, qui bloque environ 300 syscalls réputés dangereux tout en préservant la compatibilité applicative large. C'est le point de départ recommandé pour tout workload.Localhost— charge un profil JSON personnalisé stocké sur le nœud, pour les cas oùRuntimeDefaultest trop permissif ou trop restrictif.Unconfined— aucun filtrage. À réserver aux workloads d'infrastructure qui en ont un besoin documenté (agentsagentIASystème qui enchaîne des appels d'outils de façon autonome pour atteindre un objectif : il planifie, agit, observe, recommence. Sa fiabilité décroît exponentiellement avec le nombre d'étapes.Voir dans le glossaire de monitoring bas niveau, par exemple).
securityContext:
seccompProfile:
type: RuntimeDefault
Ne passez à un profil
Localhostsur mesure qu'après avoir constaté un besoin réel — un appel système légitime bloqué parRuntimeDefault— plutôt que par anticipation. Construire un profil trop restrictif à l'aveugle casse des comportements applicatifs difficiles à diagnostiquer, souvent des semaines après le déploiement.
Pod Security Standards et Pod Security Admission
Configurer un securityContext correct pod par pod ne suffit pas à sécuriser un cluster : rien n'empêche une équipe d'oublier, ou un manifeste tiers d'ignorer ces bonnes pratiques. C'est le rôle des Pod Security Standards (PSS), trois profils normalisés par la communauté Kubernetes :
| Niveau | Portée | Usage typique |
|---|---|---|
| Privileged | Aucune restriction | Composants d'infrastructure (CNI, CSI, monitoring bas niveau) |
| Baseline | Bloque les élévations de privilèges connues (privileged: true, capabilities dangereuses, hostNetwork, hostPID) |
Workloads legacy en transition |
| Restricted | Applique les meilleures pratiques de durcissement (runAsNonRoot, capabilities drop: ALL, seccomp obligatoire, allowPrivilegeEscalation: false) |
Workloads applicatifs standards |
Ces profils sont appliqués par le contrôleur d'admission Pod Security Admission (PSA), intégré nativement à l'API server depuis Kubernetes 1.25 (GA), en remplacement de PodSecurityPolicy (PSP), retiré à cette même version. PSA s'active par des labels sur le namespace :
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Le mode enforce rejette à l'admission tout pod non conforme ; audit journalise sans bloquer ; warn affiche un avertissement côté client kubectl. Combiner les trois permet une migration progressive : on active warn et audit en restricted avant de faire évoluer enforce, ce qui donne de la visibilité sur l'impact sans casser les déploiements existants.
La suppression de PodSecurityPolicy en 1.25 a laissé certains clusters migrés à la hâte sans équivalent PSA effectif — le namespace existe, mais sans labels d'enforcement, aucune politique n'est réellement appliquée. Vérifiez systématiquement les labels PSA après une migration depuis PSP, l'absence de politique est silencieuse et ne génère aucune alerte par défaut.
Un point souvent mal compris : PSA évalue les pods au moment de l'admission. Changer le label enforce d'un namespace ne remet pas en conformité les pods déjà en cours d'exécution — seuls les nouveaux pods, ou ceux recréés (rolling update, suppression manuelle), sont contrôlés contre la nouvelle politique.
Checklist de durcissement
- Définir
runAsNonRoot: trueet unrunAsUserexplicite sur chaque pod. - Positionner
allowPrivilegeEscalation: falsesur chaque conteneur. - Passer
readOnlyRootFilesystem: true, en complétant par desemptyDirpour les répertoires réellement inscriptibles. - Retirer toutes les capabilities par défaut (
drop: ["ALL"]) et n'en rajouter que sur preuve de besoin. - Activer
seccompProfile.type: RuntimeDefaultcomme plancher minimal. - Bannir
privileged: true,hostNetwork,hostPID,hostIPCsauf justification d'infrastructure documentée. - Appliquer le label
pod-security.kubernetes.io/enforce: restrictedsur les namespaces applicatifs. - Auditer les pods existants après tout changement de politique PSA — l'enforcement n'est pas rétroactif.
Pièges fréquents
Le premier piège est l'oubli du fsGroup : un conteneur qui tourne avec un UID non root mais tente d'écrire sur un volume monté avec des permissions root échoue avec une erreur de permission difficile à relier au security context si on ne connaît pas ce mécanisme.
Le deuxième est de considérer runAsNonRoot comme suffisant en soi, sans vérifier que l'image le supporte réellement — certaines images publiques ne définissent aucun utilisateur non root et nécessitent un Dockerfile personnalisé pour être conformes.
Le troisième, plus insidieux, concerne les clusters multi-tenants où un namespace Baseline côtoie un namespace Restricted : l'absence d'uniformité crée une fausse impression de sécurité globale alors que seule une partie du cluster est réellement durcie. La politique PSA doit être pensée au niveau du cluster, pas namespace par namespace de façon ad hoc.
L'essentiel à retenir
Un conteneur n'est pas une machine virtuelle : il partage le noyau du nœud hôte, et son isolation repose entièrement sur des mécanismes Linux configurables — namespaces, capabilities, seccomp — que Kubernetes expose via le securityContext. Ce chapitre détaille ces mécanismes, montre comment les combiner pour appliquer le principe du moindre privilège, et explique le rôle de Pod Security Admission dans l'application de politiques cohérentes à l'échelle du cluster. Il se termine par une checklist de durcissement et les pièges les plus fréquents constatés en production.
- Namespaces Linux (PID, NET, MNT, UTS, IPC, USER)
- Security Context (pod et conteneur)
- runAsNonRoot et allowPrivilegeEscalation
- Capabilities Linux et principe du moindre privilège
- Profils seccomp (RuntimeDefault, Localhost)
- Pod Security Standards (Privileged, Baseline, Restricted)
- Pod Security Admission (enforce, audit, warn)
- readOnlyRootFilesystem et fsGroup
Questions fréquentes
Un pod avec `runAsNonRoot: true` est-il automatiquement sécurisé ?
Pourquoi mon pod refuse-t-il de démarrer avec `runAsNonRoot: true` alors qu'il fonctionnait avant ?
Quelle est la différence entre les capabilities Linux et seccomp ?
Faut-il appliquer le niveau Restricted à tous les namespaces d'un cluster ?
Pod Security Admission bloque-t-il aussi les pods au moment du build de l'image ?
Pourquoi `readOnlyRootFilesystem: true` casse-t-il parfois une application qui fonctionnait sans cette option ?
Le namespace USER de Kubernetes remplace-t-il le besoin de `runAsNonRoot` ?
Comment savoir si les labels Pod Security Admission d'un namespace sont réellement appliqués ?
Progression sauvegardée dans votre navigateur.
Quiz de validation
Quiz Player
Quiz de validation
Plusieurs réponses possibles — validez ensuite.
Vrai ou faux.
Quiz indisponible (données invalides).