Pods privilegiados y escape de contenedores: los indicios
Detecta la preparación de un escape de contenedor en los audit logs de Kubernetes: pods privilegiados, hostPID, hostPath de / y DaemonSets en kube-system.
TL;DR. El audit log no puede ver cómo ocurre un escape de contenedor, pero sí ve el spec del pod que lo vuelve trivial. Busca escrituras de workloads (pods, deployments, daemonsets, jobs, cronjobs…) cuyo cuerpo fije securityContext.privileged: true, añada SYS_ADMIN, comparta hostPID / hostNetwork / hostIPC o monte un hostPath de /, /etc, /var/lib/kubelet o un socket del runtime de contenedores. Un contenedor privilegiado con el sistema de archivos raíz o el espacio de PIDs del nodo equivale a tomar el nodo. Sigue con el exec que lo aprovechó (chroot /host, nsenter). Todo esto requiere los cuerpos de las solicitudes en el log.
ATT&CK lo divide en T1610, Deploy Container y T1611, Escape to Host. En Kubernetes ambos suelen ocurrir con menos de un minuto de diferencia, y el audit log registra el primero completo.
Por qué importan estos ajustes
Un contenedor es un conjunto de namespaces de Linux y restricciones sobre un kernel compartido. Cada uno de estos ajustes del pod quita una capa:
| Ajuste | Qué le da al contenedor |
|---|---|
securityContext.privileged: true | Todas las capabilities, acceso a los dispositivos del host; puede montar el disco del host |
capabilities.add: ["SYS_ADMIN"] (o ALL) | Casi todo lo que da privileged, montajes incluidos |
hostPID: true | Ve y puede enviar señales a todos los procesos del nodo; nsenter -t 1 entra en el host |
hostNetwork: true | Usa la pila de red del nodo: servicios locales, endpoint de metadatos cloud, captura de tráfico |
hostIPC: true | Comparte el namespace IPC del nodo |
hostPath de / | Todo el sistema de archivos del nodo: credenciales del kubelet, volúmenes de otros pods, chroot |
hostPath de /var/run/docker.sock, containerd.sock, crio.sock | Habla con el runtime de contenedores: arranca contenedores fuera de Kubernetes |
hostPath de /etc, /root, /var/lib/kubelet | Configuración del host, claves SSH, credenciales del kubelet y secrets de los pods |
Las buenas prácticas de RBAC de Kubernetes señalan que el permiso para crear workloads o PersistentVolumes puede incluir la creación de volúmenes hostPath y, por tanto, acceso al sistema de archivos del host. El perfil Baseline de los Pod Security Standards prohíbe todos los ajustes anteriores por ese motivo.
Qué muestra el audit log
Un DaemonSet ficticio, recortado a lo que importa:
{
"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 }
}
Una imagen corriente, un nombre que imita a kube-proxy, el namespace kube-system y todos los ajustes de escape a la vez. Un DaemonSet pone una copia en cada nodo, así que no es un nodo tomado, son todos.
Quién lo creó: la persona, no el controlador
Los pods los crea el controlador de DaemonSet, ReplicaSet o Job, una identidad de sistema. Si solo miras las creaciones de pods, atribuirás el pod a system:serviceaccount:kube-system:daemon-set-controller. La identidad responsable es la que escribió el objeto workload. El analizador de la página principal lee la plantilla del pod dentro de Deployments, DaemonSets, StatefulSets, ReplicaSets, Jobs y CronJobs, así que los hallazgos apuntan a la persona o al token que escribió el spec.
Creado no siempre significa en ejecución
Con Pod Security Admission, el modo enforce se aplica a los pods, no a los objetos workload: la documentación de PSA indica que los recursos de workload solo reciben audit y warn. Por eso un DaemonSet privilegiado puede aceptarse (201) mientras se rechaza cada pod que intenta crear. Revisa los eventos create de pods del controlador y sus códigos de respuesta antes de concluir que el escape ocurrió. En modo audit, PSA añade al evento de auditoría una anotación que describe la infracción, lo cual es una señal útil por sí misma.
Detecciones
| Regla | Severidad | Lógica |
|---|---|---|
| Escape de contenedor hacia el nodo | Crítica | Contenedor privilegiado y (hostPath / o hostPID) |
| Contenedor privilegiado desplegado | Alta | privileged: true o SYS_ADMIN / ALL añadidos |
| Ruta sensible del host montada | Alta | hostPath de /, /etc, /etc/kubernetes, /root, /home, /proc, /sys, /dev, /boot, /var/lib/kubelet o un socket de Docker, containerd o CRI-O |
| El workload comparte el namespace de PID / red / IPC del nodo | Media | hostPID, hostNetwork o hostIPC |
| DaemonSet creado en kube-system | Alta | Cualquier DaemonSet creado ahí por una identidad que no es de sistema |
Todas se aplican a create, update y patch: parchear un Deployment existente para añadir un hostPath es tan eficaz como crear uno nuevo, y menos visible.
Espera coincidencias legítimas. Plugins CNI, drivers CSI, agentes de logs, node exporters y agentes de seguridad corren como DaemonSets privilegiados con montajes del host. Normalmente los instala una identidad de pipeline conocida, en un momento conocido y desde un registry conocido. Uno nuevo creado con kubectl desde un portátil, o por una cuenta de servicio que nunca despliega nada, no lo es.
Luego busca el exec
A la preparación le sigue el uso. Filtra los eventos pods/exec de los pods creados a partir del workload sospechoso (los nombres empiezan por el del workload) y lee los comandos:
chroot /host shochroot /host bash: una shell en el sistema de archivos del nodo;nsenter --target 1 --mount --uts --ipc --net --pid: una shell en los namespaces del nodo a través del PID 1;cat /host/var/lib/kubelet/…,ls /host/etc/kubernetes: búsqueda de credenciales;crictl,ctrodockercontra un socket montado: contenedores arrancados fuera de Kubernetes.
Consulta cómo detectar kubectl exec para ver cómo aparecen los comandos en requestURI. Si no hay ningún exec, puede que el propio comando del contenedor haga el trabajo: lee command y args en el spec.
Lo que no verás
Una vez en el nodo, el atacante queda fuera de la vista del API server. Procesos, claves SSH nuevas, reutilización de credenciales del kubelet y contenedores arrancados directamente a través del socket del runtime no generan eventos de auditoría de Kubernetes. Necesitas evidencia a nivel de nodo: detección de runtime como Falco, EDR, los logs del propio nodo y un snapshot del disco. El artículo sobre limitaciones explica cómo combinar ambas cosas. El analizador puede cargar alertas de Falco en JSON junto a los audit logs y ponerlas en la misma línea de tiempo.
Remediación
- Guarda los specs de los workloads como evidencia, luego borra los workloads y busca copias en otros namespaces.
- Trata como comprometido cada nodo que ejecutó el pod: cordon, drain, snapshot, sustitución. Rota los certificados del kubelet y el rol de instancia cloud o la identidad administrada que usaba el nodo.
- Rota los secrets de cada pod que corrió en esos nodos: se podían leer desde el host.
- Aplica Pod Security Admission
baselineorestricteden todos los namespaces, incluidokube-systemcon exenciones estrechas y nominales. - Restringe quién puede crear workloads en
kube-system.