Panorama#
Kubernetes ganó el mercado de la orquestación de contenedores a una velocidad que dejó atrás su postura de
seguridad: no viene endurecido de fábrica. Un clúster instalado con kubeadm arranca sin network policy
activa, sin pod security, con los secrets sin cifrar y con la red completamente plana. Cada control que
importa —el aislamiento del contenedor, el firewall entre pods, la política de admisión, la firma de las
imágenes— es opt-in, y esa laxitud por defecto es la superficie que explota el adversario. El material de este
capítulo es threat-driven: sigue la disciplina del libro de referencia de personificar al atacante —el pirata
Captain Hashjack, un criminal motivado con extorsión y robo en mente— y presentar cada técnica ofensiva junto
a su contramedida. Es, por diseño, un capítulo púrpura: ataque y detección van juntos en cada sección.
La tesis que ordena el capítulo retoma y multiplica la que vertebró la web moderna (3.5): si allí cada comodidad reubicaba una frontera de confianza, aquí la arquitectura entera es una pila de fronteras de confianza apiladas, todas laxas por defecto —la imagen, el contenedor, el pod, el nodo/host, la red, la API del plano de control—. El atacante que compromete un pod no busca «la vulnerabilidad»: recorre esas fronteras hacia arriba, del RCE dentro de un contenedor al escape al host, y de ahí al robo de la identidad —el service account— que gobierna todo el clúster. La unidad de análisis es el pod, y el peor caso es el container breakout, que —esto es lo central— ocurre más por misconfiguración que por exploit de kernel. La escalada de este capítulo es la forma cloud-native de la escalada de privilegios (3.9): mismo principio —enumerar y encontrar la frontera que quedó abierta—, un piso de abstracción más arriba.
flowchart TD I["Imagen\n(CVEs · cadena de suministro)"] --> C["Contenedor\n(namespaces/cgroups/caps/LSM)"] C --> P["Pod\n(trust boundary · securityContext)"] P --> H["Nodo / Host\n(container breakout)"] H --> R["Red plana\n(lateral · secrets en etcd)"] R --> A["API / plano de control\n(service account · RBAC)"] A -.->|"el atacante sube;\ncada frontera está apagada por default"| I classDef f fill:#1e293b,stroke:#475569,color:#e2e8f0; class I,C,P,H,R,A f;
En términos de MITRE ATT&CK for Containers el capítulo recorre el acceso inicial por imagen o aplicación vulnerable, la ejecución y persistencia en el clúster, la evasión del aislamiento (escape to host, T1611), el acceso a credenciales (service account tokens) y el descubrimiento por la API. Cierra el bloque RED de explotación: es el último capítulo de P3.
El pod como frontera de confianza#
Un contenedor no es una entidad del kernel sino una ficción de espacio de usuario: el resultado de combinar
namespaces (aíslan la vista de procesos, red, montajes), cgroups (limitan recursos —y no son un límite de
seguridad en su versión 1), capabilities de Linux y módulos de seguridad (LSM: AppArmor, SELinux) alrededor de
un proceso arrancado desde el sistema de archivos de una imagen. Su seguridad tiene dos mitades: el contenido de
la imagen (sus CVEs) y la configuración de ejecución (el securityContext).
El pod —el conjunto de contenedores que comparten namespaces de red e IPC— es la unidad de despliegue y
una frontera de confianza en sí misma: como sus contenedores comparten la red, un atacante en uno puede
atacar los sockets privados de los otros, incluida la interfaz loopback 127.0.0.1. Los sidecars e init
containers suelen correr con privilegios elevados y son de interés para el adversario.
Tras un RCE en un contenedor —el foothold de Hashjack fue una versión vulnerable de Struts (la misma librería
del breach de Equifax, CVE-2017-5638) explotada con Metasploit—, el flujo de reconocimiento es fijo: enumerar
el securityContext y las capabilities (amicontained), leer el token del service account en
/var/run/secrets/kubernetes.io/serviceaccount, obtener la IP de la API del entorno del pod, y enumerar los
servicios del clúster —que CoreDNS entrega enteros por consulta DNS—.
# Enumeración del clúster desde un pod comprometido
env | grep KUBE # KUBERNETES_SERVICE_HOST=10.7.240.1 (la API)
dig +noall +answer srv any.any.svc.cluster.local # CoreDNS lista TODOS los servicios del clúster
cat /var/run/secrets/kubernetes.io/serviceaccount/token # la identidad del pod
La primera línea de defensa tras el RCE es un securityContext correcto: correr como no-root (runAsNonRoot,
runAsUser > 10000), allowPrivilegeEscalation: false, dropear todas las capabilities y readmitir solo las
imprescindibles, readOnlyRootFilesystem, y activar seccomp/AppArmor/SELinux. La razón es que el precursor de
casi todo escape es conseguir root dentro del contenedor (y a menudo CAP_SYS_ADMIN, la de montaje): sin root,
muchos breakouts se anulan. Las capabilities especialmente peligrosas —CAP_SYS_ADMIN (montaje),
CAP_SYS_MODULE (cargar módulos de kernel), CAP_SYS_PTRACE (debug de otros procesos), CAP_NET_RAW (activa
por defecto en runc, habilita sniffing y spoofing), CAP_BPF— son las que un securityContext restrictivo
debe negar. Herramientas como kubesec y kubeaudit puntúan una configuración antes de desplegarla.
privileged: true es, en palabras de Liz Rice, «el más peligroso de la historia de la computación»: deja
solo el namespace de procesos, desactiva los LSM, desenmascara /dev y otorga todas las capabilities. Un
contenedor privileged no está aislado del host de forma significativa; es la puerta de escape más directa. Junto
a él, los flags de host —hostPID, hostNetwork, hostIPC, hostPath— cada uno anula una dimensión distinta
del aislamiento. Ninguno debería llegar a producción sin una justificación explícita y un control de admisión que
lo verifique.El escape al host#
El container breakout —escapar del contenedor al host— es el peor caso porque el host es tan poderoso como todos los workloads que ejecuta: root en el nodo accede a las identidades de todos ellos. Los autores lo definen ampliamente: no solo el exploit de kernel, sino cualquier evasión del aislamiento, y la vía más común es la misconfiguración que abre un camino legítimo al escape. Conviene situarlo en un espectro: el contenedor comparte el kernel del host (lo acotan namespaces y LSM, pero no lo aíslan del kernel), la máquina virtual arranca un kernel guest bajo un hypervisor, y los sandboxes de nueva generación se interponen en medio para recortar la superficie de syscalls que un workload hostil puede tocar.
Los escapes concretos, ordenados de más a menos privilegio exigido:
- Contenedor
privileged. Con acceso al disco del host, se monta y se escribe una clave SSH enauthorized_keysde root, o se manipula cualquier archivo del sistema (mount /dev/xvda1 /mnt/). hostPID+privileged. Permite entrar al mount namespace del PID 1 connsenter—el one-liner canónico de Duffie Cooley e Ian Coldwater— y transportar la shell al sistema de archivos del host.hostPathescribible. Montar un directorio del host, incluso uno aparentemente inocuo como/var/log(Aqua, kube-pod-escape), expone el sistema de archivos completo por symlinks; con escritura, se deja un manifiesto de pod estático en/etc/kubernetes/manifests/y elkubeletlo instancia —el mismo vector de persistencia contenedor→host que ya cerraba la persistencia en Linux (3.14)—.runcCVE-2019-5736 (/proc/self/exe). Un entrypoint que apunta con symlink a/proc/self/exe—el binarioruncdel host— permite sobrescribirlo desde dentro y ejecutar en el host la próxima vez que arranca un contenedor. Es explotable enteramente desde una imagen maliciosa, con sesgo cloud-native de cadena de suministro.- Abuso de user namespaces. Fuente histórica de escalada (CVE-2018-18955 con
CAP_SYS_ADMINen el userns → leer/etc/shadowfuera de él); no están habilitados en Kubernetes.
# Escape canónico: nsenter con hostPID + privileged (one-liner de Duffie Cooley)
kubectl run r00t --restart=Never -ti --rm --image lol --overrides \
'{"spec":{"hostPID":true,"containers":[{"name":"1","image":"alpine",
"command":["nsenter","--mount=/proc/1/ns/mnt","--","/bin/bash"],
"stdin":true,"tty":true,"securityContext":{"privileged":true}}]}}'
La defensa de fondo es reducir la superficie de kernel expuesta, y su operador en Kubernetes es
RuntimeClass: agendar los workloads sensibles o no confiables sobre un runtime endurecido. gVisor interpone
un «kernel de espacio de usuario» —el proceso Sentry reimplementa más de 235 syscalls y solo necesita ~53–68
del host—; Firecracker es un VMM mínimo en Rust sobre KVM (36 syscalls, sin BIOS ni PCI, el motor de AWS
Lambda); Kata Containers envuelve el contenedor en una VM KVM dedicada. A eso se suman seccomp por defecto
(--seccomp-default desde v1.22), AppArmor/SELinux, ejecución rootless donde sea viable, y el parcheo agresivo
del kernel —porque los escapes por CVE dependen de un kernel o un runtime sin parchear, el mismo principio de
1-day del patch diffing (3.8)—. El riesgo residual: los sandboxes son código nuevo
con bugs nuevos, y aún admiten correrse privileged.
La red plana y los secretos#
Los valores por defecto de red son el mayor vector plano del clúster: topología chata —todo pod ve y habla
con todo pod—, sin identidad de workload y sin cifrado en el cable. «Todo el tráfico permitido» es una
sola superficie de ataque gigante: desde un único pod comprometido se barre el clúster entero, y con
CAP_NET_RAW (activa por defecto) se hace ARP y DNS spoofing para interponerse entre pods —el sustrato del
movimiento lateral (3.13) llevado al plano del contenedor—.
La contramedida base es la NetworkPolicy default-deny: podSelector: {} + policyTypes: [Ingress] aísla
todo el ingress de un namespace. Un matiz operativo importante: Kubernetes define la política pero un
proveedor la aplica —Calico o Cilium, este último sobre eBPF—; sin proveedor, la NetworkPolicy es letra
muerta. Por encima se añade un service mesh (Linkerd, Istio) que da mTLS transparente e identidad, y SPIFFE/
SPIRE para credenciales de workload de vida corta. La defensa tiene grietas conocidas: en Istio/Envoy un
contenedor con UID 1337 bypassa el sidecar.
Del lado del almacenamiento, el activo es el dato, y aquí la laxitud es aguda: los secrets se guardan sin
cifrar en etcd por defecto y se montan por tmpfs desde el kubelet, de modo que root en el nodo lee todos
los secrets de todos los pods (mount | grep secret). La postura correcta es cifrarlos at-rest con un KMS
o Vault. Y el objeto más codiciado es el token del service account: es la identidad del pod y el vector
de escalada número uno. Tras un RCE se busca primero y se enumeran sus permisos; un SA sobre-privilegiado abre la
API del clúster y, a menudo, el IAM del proveedor cloud (bases de datos, storage, backups).
# Con el token del service account robado: enumerar permisos y buscar privesc
kubectl auth can-i --list
rakkess # tabla LIST/CREATE/UPDATE/DELETE por recurso → secrets ✔✔✔✔ = over-privileged
Los service account tokens estándar son JWT que nunca expiran; los bound tokens añaden expiry y
audience, y el NodeAuthorizer limita el radio de impacto. Los pods que no necesitan hablar con la API deben
firewallearse con NetworkPolicy para que su token no sirva de trampolín.
Identidad y política: RBAC y admission control#
Si la red es la frontera lateral, RBAC es la frontera de la identidad, y el admission control es la muralla
que Kubernetes no trae encendida. El modelo de autorización es directo: tras autenticar, la API decide con
credencial + recurso + verbo (403 si falla). Los verbos se dividen en read-only (get, list) y read-write
(create, update, patch, delete), con alcance de namespace (Role) o de clúster (ClusterRole).
Kubernetes previene la escalada trivial —solo se puede crear o editar un rol si ya se poseen todos sus permisos—,
así que los ataques reales no son CVEs sino permisos demasiado laxos: un edit donde bastaba un list, un
componente histórico sobre-privilegiado (Helm 2 / Tiller como confused deputy), y sobre todo el acceso
transitivo: un rol que permite actuar sobre otra service account más poderosa es una ruta de escalada.
Los verbos escalate, bind e impersonate rompen la contención por diseño.
La muralla es el admission control, ausente por defecto: un webhook que valida o muta cada recurso antes de
persistirlo en etcd. Los Pod Security Standards (sucesores de los Pod Security Policies, ya deprecados)
rechazan pods privileged, con namespaces de host o root. Por encima, los motores genéricos —OPA/
Gatekeeper (política como código en el lenguaje Rego) y Kyverno (política en YAML, sin Rego)— expresan
cualquier regla de organización. Del lado defensivo se automatiza el least-privilege con audit2rbac (deriva
roles mínimos del audit log real) y se hace legible el bosque de roles con rback o krane.
privileged, un mount de host). La defensa lo cubre con el modo auditing y con
alertas sobre la caída del propio motor.La cadena de suministro: el vector por proxy#
La frontera más externa —y la que Hashjack prefiere cuando el perímetro resiste— es la cadena de suministro: comprometer algo que la víctima consume (una imagen base, un paquete OSS, una dependencia, el pipeline de CI/CD) para ejecutar código dentro de su perímetro de confianza. El principio es la transitividad de la confianza, que Ken Thompson formuló en 1983: «no se puede confiar en código que no se creó por completo uno mismo». El caso maestro es SUNBURST/SolarWinds: el malware SUNSPOT infectó el pipeline de compilación, alteró el código fuente justo antes de compilarlo y lo revirtió justo después, y dejó que el propio CI/CD firmara el binario para que los consumidores confiaran; pasaron unos seis meses hasta detectar las infecciones. La lección: la infraestructura de build es producción, aunque rara vez se la trate como tal, y hay que asumirla comprometida por defecto.
Del lado ofensivo, más allá de infiltrar el build, se trojaniza una imagen —dockerscan inyecta una
reverse shell vía LD_PRELOAD, el mismo enganche del enlazador dinámico que vimos como
persistencia en Linux (3.14) y cuya evasión de escáneres es la de la
evasión de AV/EDR (3.11)— o se abusa de los registries públicos por typosquatting y
account-takeover (los casos npm event-stream, eslint-scope: cientos de millones de descargas envenenadas).
Una vez dentro, la persistencia en Kubernetes es distinta: como los cambios al sistema de archivos del
contenedor se pierden al reiniciar, no basta escribir en el contenedor; el atacante persiste con pods
estáticos, webhooks de admisión con puerta trasera, hooks de ciclo de vida (postStart/preStop), o sumando
el nodo a un botnet o C2 (3.10) —ninguna posible sin RBAC excesivo—.
La defensa sube por una escalera de controles. El mínimo viable es escanear CVEs de dependencias (trivy
sobre imagen, filesystem o repositorio). Por encima, la firma criptográfica traslada la confianza a una
clave: sigstore —cosign firma cualquier artefacto OCI, rekor es un ledger de transparencia append-only
y fulcio una CA gratuita por identidad OIDC— con un admission controller (portieris, Binary Authorization)
que rechaza en el despliegue lo que no esté firmado. El SBOM (syft → CycloneDX) es el packing list
verificable de lo que contiene una imagen, y el marco graduado SLSA (niveles 0–4) mide la madurez del build
—provenance no falsificable, builds herméticos y reproducibles—. Y menos superficie es menos ataque: las
imágenes base mínimas (Scratch, Distroless) eliminan la shell y las utilidades que el atacante
necesitaría —de hecho, quitar Bash de la imagen elimina de un golpe la reverse shell por /dev/tcp que solo
Bash ofrece—.
Defensa y detección: el IDS de runtime#
La última línea de la defensa en profundidad asume que el adversario ya entró y busca desviaciones del
comportamiento estable del workload. Aquí los contenedores tienen una ventaja de fidelidad que los hosts
tradicionales no: un contenedor es de propósito único, así que su «comportamiento permitido» es un conjunto muy
pequeño y una desviación resalta de inmediato. Pero también un problema: el IDS tradicional —Snort/Suricata en
red, auditd en syscalls— es previo a los contenedores y no entiende namespaces (auditd es pesado y no
distingue por contenedor).
La respuesta cloud-native es el IDS basado en eBPF, que corre como código confiable dentro del kernel y por
eso es rápido y granular —el mismo subsistema que arma al atacante en los rootkits de eBPF de 3.14, usado aquí
por el defensor con la ventaja de operar bajo el engaño—. La referencia es Falco (y su par Tracee):
intercepta syscalls como execve y open y alerta sobre lo anómalo —el disparo emblemático es «a shell was
spawned in a container with an attached terminal»—, con reglas de comunidad (puertos de entrada esperados por
componente, procesos permitidos por contenedor). El resto del arsenal defensivo:
- Forense que capture también la memoria.
kube-forensicstoma una instantánea del pod (docker inspect/diff/export) pero no la memoria, donde viven los implants que nunca tocaron disco; para eso, un volcado de/proc/PID/mapscon GDB. La herramienta ofensivatruffleprochace exactamente esto para cosecharSECRET_KEYy tokens de la RAM, recordando que root en un namespace de procesos puede volcar la memoria de cualquier proceso de ese namespace. - Honeypots y canary tokens. Un recurso-trampa que las aplicaciones legítimas nunca tocan —desplegado como
DaemonSet, uno por nodo, con un nombre atractivo como
myapp-data— y canary tokens (claves AWS/Slack, URLs, registros DNS falsos) sembrados en producción: tripwires que alertan al ser accedidos. - El audit log de la API, enviado fuera del clúster. Registra cada petición (niveles
None→RequestResponse) y detecta el uso de honeytokens, horarios o IPs anómalas, o un service accountdefaultpidiendo todos los secrets. Pero debe blindarse: los payloads de más de 256 KB no se guardan enetcd—un atacante con acceso a la API opera de forma sigilosa con entradas sobredimensionadas—, y los logs locales son manipulables, así que hay que enviarlos a un webhook remoto append-only (--audit-webhook-config-file).
startswith quay.io/sysdig matcheaba una imagen
…-que-termina-en-sysdig/agent). La lección púrpura es que el catálogo de reglas debe validarse continuamente
con pruebas de red team dirigidas —el ejercicio purple que valida que los controles disparan— y no darse por
correcto porque exista.Toda esta telemetría —la shell inesperada en un contenedor, el mount/nsenter anómalo, la escritura en
/etc/kubernetes/manifests/, la lectura masiva de secrets, el beaconing de salida— se correlaciona en el
SIEM como una sola historia de intrusión cloud-native, no como eventos sueltos; la caza y el forense de
contenedores del lado blue se desarrollan en la detección de runtime de
contenedores (P5), y la correlación general de eventos se apoya en el motor descrito en
4.9 · Defensa de AD.
flowchart TD B["Intrusión cloud-native\n(RCE en pod → escape → API)"] B --> RT["IDS de runtime (eBPF)\nFalco/Tracee: shell en contenedor · execve/open anómalo"] B --> AU["Audit log de la API → webhook remoto\n(SA default pide todos los secrets · honeytoken)"] B --> FS["FIM / escape\n(nsenter · mount · /etc/kubernetes/manifests)"] B --> ME["Forense con memoria\n(GDB /proc/PID/maps · no solo disco)"] RT & AU & FS & ME --> X["Correlación SIEM\n= una historia cloud-native"] X --> P["→ detección de runtime (P5)\n→ securityContext · RuntimeClass · NetworkPolicy · RBAC · firma"]
Referencias#
red-infra/hacking-kubernetes/cap-01— Threat model + ataques a nivel pod:securityContext, capabilities peligrosas, flags de host, enumeración por CoreDNS y service account, y la matriz de amenazas de Microsoft.red-infra/hacking-kubernetes/cap-02— Aislamiento de runtime y escapes: el espectro contenedor/VM/sandbox, user namespaces, rootless, gVisor/Firecracker/Kata,RuntimeClass, yruncCVE-2019-5736.red-infra/hacking-kubernetes/cap-03— Aplicaciones y cadena de suministro: SUNBURST/SUNSPOT,trivy, SBOM (syft), sigstore (cosign/rekor/fulcio), SLSA, imágenes base mínimas, typosquatting y persistencia.red-infra/hacking-kubernetes/cap-04— Exposición de red y almacenamiento: topología plana,NetworkPolicydefault-deny, service mesh/mTLS, secrets sin cifrar enetcd,nsenter/hostPath, y service account tokens.red-infra/hacking-kubernetes/cap-05— Abuso de RBAC y políticas: modelo RBAC, admission control, OPA/ Gatekeeper y Kyverno, el fail-open, y el least-privilege automatizado (audit2rbac).red-infra/hacking-kubernetes/cap-06— Detección de intrusiones y runtime security: Falco/Tracee (eBPF), forense con memoria (truffleproc), honeypots/canary tokens, audit log remoto y la evasión de la detección.- MITRE ATT&CK for Containers (TA/Matrix) con T1611 Escape to Host; los proyectos Falco, Tracee, sigstore, gVisor y Trivy como referencia de las herramientas.