Pods privilégiés et évasion de conteneur : les indices
Repérer une évasion de conteneur en préparation dans les logs d'audit Kubernetes : pods privilégiés, hostPID, hostPath sur /, DaemonSets dans kube-system.
TL;DR. Le log d'audit ne voit pas une évasion de conteneur se produire, mais il voit la spec de pod qui la rend triviale. Cherchez les écritures de workloads (pods, deployments, daemonsets, jobs, cronjobs…) dont le corps fixe securityContext.privileged: true, ajoute SYS_ADMIN, partage hostPID / hostNetwork / hostIPC, ou monte un hostPath sur /, /etc, /var/lib/kubelet ou un socket de runtime de conteneurs. Un conteneur privilégié avec le système de fichiers racine ou l'espace PID du nœud, c'est la prise de contrôle du nœud. Enchaînez avec l'exec qui l'a exploité (chroot /host, nsenter). Tout cela exige les corps de requête dans le log.
ATT&CK découpe cela en T1610, Deploy Container et T1611, Escape to Host. Dans Kubernetes, les deux se suivent en général à moins d'une minute, et le log d'audit enregistre la première en entier.
Pourquoi ces réglages comptent
Un conteneur, c'est un ensemble de namespaces Linux et de restrictions sur un noyau partagé. Chacun de ces réglages de pod retire une couche :
| Réglage | Ce qu'il donne au conteneur |
|---|---|
securityContext.privileged: true | Toutes les capabilities, accès aux périphériques de l'hôte ; peut monter le disque de l'hôte |
capabilities.add: ["SYS_ADMIN"] (ou ALL) | L'essentiel de ce que donne privileged, montages compris |
hostPID: true | Voit et peut signaler tous les processus du nœud ; nsenter -t 1 entre dans l'hôte |
hostNetwork: true | Utilise la pile réseau du nœud : services locaux, endpoint de métadonnées cloud, écoute du trafic |
hostIPC: true | Partage l'espace IPC du nœud |
hostPath sur / | Tout le système de fichiers du nœud : identifiants du kubelet, volumes des autres pods, chroot |
hostPath sur /var/run/docker.sock, containerd.sock, crio.sock | Parle au runtime de conteneurs : lance des conteneurs hors de Kubernetes |
hostPath sur /etc, /root, /var/lib/kubelet | Configuration de l'hôte, clés SSH, identifiants du kubelet et secrets des pods |
Les bonnes pratiques RBAC de Kubernetes notent que le droit de créer des workloads ou des PersistentVolumes peut inclure la création de volumes hostPath, donc l'accès au système de fichiers de l'hôte. Le profil Baseline des Pod Security Standards interdit tous les réglages ci-dessus pour cette raison.
Ce que montre le log d'audit
Un DaemonSet fictif, réduit à l'essentiel :
{
"verb": "create",
"user": { "username": "system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard" },
"objectRef": { "resource": "daemonsets", "namespace": "kube-system", "name": "kube-proxy-monitor", "apiGroup": "apps" },
"requestObject": {
"spec": { "template": { "spec": {
"hostPID": true,
"hostNetwork": true,
"containers": [{
"name": "monitor",
"image": "docker.io/library/alpine:3.20",
"securityContext": { "privileged": true },
"volumeMounts": [{ "name": "host", "mountPath": "/host" }]
}],
"volumes": [{ "name": "host", "hostPath": { "path": "/", "type": "Directory" } }]
} } }
},
"responseStatus": { "code": 201 }
}
Une image banale, un nom qui imite kube-proxy, le namespace kube-system, et tous les réglages d'évasion à la fois. Un DaemonSet place une copie sur chaque nœud : ce n'est pas un nœud pris, ce sont tous les nœuds.
Qui l'a créé : la personne, pas le contrôleur
Les pods eux-mêmes sont créés par le contrôleur DaemonSet, ReplicaSet ou Job, une identité système. Si vous ne regardez que les créations de pods, vous attribuerez le pod à system:serviceaccount:kube-system:daemon-set-controller. L'identité responsable est celle qui a écrit l'objet workload. L'analyseur de la page d'accueil lit le template de pod à l'intérieur des Deployments, DaemonSets, StatefulSets, ReplicaSets, Jobs et CronJobs : les constats pointent donc vers la personne ou le token qui a écrit la spec.
Créé ne veut pas toujours dire en cours d'exécution
Avec Pod Security Admission, le mode enforce s'applique aux pods, pas aux objets workload : la documentation PSA précise que les ressources de workload ne reçoivent que audit et warn. Un DaemonSet privilégié peut donc être accepté (201) alors que chaque pod qu'il tente de créer est rejeté. Vérifiez les événements create de pods du contrôleur et leurs codes de réponse avant de conclure que l'évasion a eu lieu. En mode audit, PSA ajoute à l'événement d'audit une annotation décrivant la violation, ce qui est un signal utile en soi.
Détections
| Règle | Sévérité | Logique |
|---|---|---|
| Évasion de conteneur vers le nœud | Critique | Conteneur privilégié et (hostPath / ou hostPID) |
| Conteneur privilégié déployé | Élevée | privileged: true ou ajout de SYS_ADMIN / ALL |
| Chemin hôte sensible monté | Élevée | hostPath sur /, /etc, /etc/kubernetes, /root, /home, /proc, /sys, /dev, /boot, /var/lib/kubelet ou un socket Docker, containerd ou CRI-O |
| Le workload partage les namespaces PID / réseau / IPC du nœud | Moyenne | hostPID, hostNetwork ou hostIPC |
| DaemonSet créé dans kube-system | Élevée | Tout DaemonSet créé là par une identité non système |
Toutes s'appliquent à create, update et patch : patcher un Deployment existant pour ajouter un hostPath est aussi efficace que d'en créer un nouveau, et moins visible.
Attendez-vous à des correspondances légitimes. Plugins CNI, drivers CSI, agents de logs, node exporters et agents de sécurité tournent en DaemonSets privilégiés avec des montages de l'hôte. Ils sont normalement installés par une identité de pipeline connue, à un moment connu, depuis un registry connu. Un nouveau DaemonSet créé avec kubectl depuis un portable, ou par un compte de service qui ne déploie jamais rien, ne l'est pas.
Cherchez ensuite l'exec
La préparation est suivie de l'usage. Filtrez les événements pods/exec sur les pods issus du workload suspect (leurs noms commencent par celui du workload) et lisez les commandes :
chroot /host shouchroot /host bash: un shell sur le système de fichiers du nœud ;nsenter --target 1 --mount --uts --ipc --net --pid: un shell dans les namespaces du nœud via le PID 1 ;cat /host/var/lib/kubelet/…,ls /host/etc/kubernetes: chasse aux identifiants ;crictl,ctroudockercontre un socket monté : conteneurs lancés hors de Kubernetes.
Voir détecter kubectl exec pour la façon dont les commandes apparaissent dans requestURI. S'il n'y a aucun exec, la commande du conteneur fait peut-être le travail elle-même : lisez command et args dans la spec.
Ce que vous ne verrez pas
Une fois sur le nœud, l'attaquant est hors du champ de vision de l'API server. Processus, nouvelles clés SSH, réutilisation des identifiants du kubelet et conteneurs lancés directement via le socket du runtime ne produisent aucun événement d'audit Kubernetes. Il vous faut des preuves au niveau du nœud : détection runtime comme Falco, EDR, logs du nœud et snapshot disque. L'article sur les limites explique comment combiner les deux. L'analyseur peut charger des alertes Falco en JSON à côté des logs d'audit et les placer sur la même chronologie.
Remédiation
- Sauvegardez les specs des workloads comme preuves, puis supprimez les workloads et cherchez des copies dans les autres namespaces.
- Considérez comme compromis chaque nœud qui a exécuté le pod : cordon, drain, snapshot, remplacement. Faites tourner les certificats du kubelet et le rôle d'instance cloud ou l'identité managée utilisée par le nœud.
- Faites tourner les secrets de chaque pod qui a tourné sur ces nœuds : ils étaient lisibles depuis l'hôte.
- Appliquez Pod Security Admission en
baselineourestrictedsur chaque namespace, y compriskube-systemavec des exemptions étroites et nominatives. - Restreignez qui peut créer des workloads dans
kube-system.