Saltar al contenido

Seguridad en Docker: los fallos de contenedor que acaban en el host

    Seguridad en Docker:

    los fallos de contenedor que acaban en el host

    Linux avanzado  |  unfantasmaenelsistema.com

    Docker resolvió un problema real: empaquetar una aplicación con sus dependencias y ejecutarla igual en cualquier sitio. Por el camino se coló una idea peligrosa: que un contenedor es «como una máquina virtual pequeña», una caja hermética de la que nada puede salir. No lo es. Un contenedor es un proceso normal del host, ejecutándose bajo el mismo kernel que todo lo demás, con unas cuantas restricciones encima. Cuando esas restricciones están mal puestas —o se quitan a propósito «para que funcione»— la distancia entre «tengo acceso al contenedor» y «tengo root en el host» puede ser de un solo comando.

    Este artículo recorre esa frontera: qué la sostiene (namespaces, cgroups, capabilities, seccomp, AppArmor), qué la rompe (banderas como –privileged, montar el socket de Docker, correr como root), y cómo se endurece un despliegue propio sin dejar de ser práctico. Sigue a los artículos de contenedores y eBPF ya publicados, ahora por el lado de la seguridad.

    Qué aprenderás en este artículo

    Por qué un contenedor no es una VM y qué separa realmente al contenedor del host · Las banderas y montajes que convierten un contenedor en acceso directo al host · Cómo funcionan las Linux capabilities, seccomp y AppArmor en Docker y cómo endurecerlos · Buenas prácticas de imagen: escaneo, usuario no root, bases mínimas, firma · Docker rootless y Podman como cambio de modelo · Escapes de contenedor conocidos a alto nivel y cómo se detectan (Falco, auditd)

    Encuadre legal y alcance — léelo antes de seguir

    Todo lo que sigue está pensado para endurecer despliegues propios o de tu organización, con autorización: servidores, laboratorios y CI/CD sobre los que tienes responsabilidad · Probar escapes de contenedor o abusar de configuraciones débiles en infraestructura ajena y sin permiso escrito es acceso ilícito y daños informáticos (en España, arts. 197 bis, 197 ter y 264 del Código Penal) · Los ejemplos usan imágenes y hosts de laboratorio; no hay exploits contra versiones concretas de runc ni payloads de evasión, de forma deliberada · El objetivo es que administradores, DevOps y equipos de plataforma reconozcan y cierren estos fallos en sus propios entornos

    1. Un contenedor no es una máquina virtual

    Una máquina virtual ejecuta su propio kernel sobre un hipervisor; la frontera entre huésped y anfitrión es de hardware virtualizado y está muy estudiada. Un contenedor no tiene kernel propio: comparte el del host. Lo que crea la ilusión de aislamiento son dos mecanismos del kernel de Linux.

    • Namespaces: dan a cada contenedor su propia vista de un recurso del sistema. El namespace PID hace que el proceso principal del contenedor se vea como PID 1 y no vea el resto de procesos del host; el de red le da sus propias interfaces; los de mount, UTS, IPC y user hacen lo propio con el sistema de ficheros, el hostname, la memoria compartida y los identificadores de usuario
    • Cgroups (control groups): limitan cuánto puede consumir el contenedor —CPU, memoria, E/S, número de PIDs— para que un contenedor no ahogue al resto

    La consecuencia práctica: namespaces y cgroups son una frontera, no una jaula. Una vulnerabilidad en el kernel del host es una vulnerabilidad para todos los contenedores a la vez. Y cualquier configuración que «perfore» un namespace —compartir el de red, el de PID o el de usuario con el host— reduce ese aislamiento de forma inmediata.

    La regla mental

    Trata la superficie del contenedor como si fuera un proceso más del host con privilegios acotados · Todo lo que le des (capabilities, montajes, namespaces del host, acceso a dispositivos) es superficie que un atacante dentro del contenedor puede intentar usar contra el host

    2. Root en el contenedor es root en el host

    Por defecto, el usuario root dentro de un contenedor (UID 0) es el mismo UID 0 del host. Las capabilities recortadas y el perfil seccomp impiden que haga muchas cosas, pero el mapeo de identidad es directo: si ese proceso consigue escapar del contenedor —por un montaje, por una capability de más, por un fallo del runtime— aterriza en el host como root.

    2.1 User namespace remapping

    El user namespace rompe ese mapeo: hace que el UID 0 del contenedor sea, por ejemplo, el UID 100000 del host, un usuario sin ningún privilegio. Se activa en el demonio con userns-remap. Es una de las defensas más eficaces y sigue infrautilizada, en parte porque complica los volúmenes con permisos y algunas imágenes mal hechas asumen ser root real.

    # /etc/docker/daemon.json  --  remapear identidades de contenedor a un rango sin privilegios
    {
      "userns-remap": "default"
    }
     
    # El demonio crea el usuario/grupo 'dockremap' y usa /etc/subuid y /etc/subgid
    # UID 0 dentro del contenedor  ->  p.ej. UID 165536 en el host (sin privilegios)

    2.2 Lo mínimo, siempre

    • Declara un usuario no privilegiado en el Dockerfile (USER appuser) y haz que la aplicación corra con él; root solo para instalar dependencias en la fase de build
    • Añade –user $(id -u):$(id -g) o `user:` en Compose cuando la imagen no lo fije
    • Monta el sistema de ficheros raíz como solo lectura (–read-only) y da rutas de escritura explícitas con tmpfs o volúmenes concretos
    • Añade –security-opt no-new-privileges para que ningún binario setuid dentro del contenedor pueda elevar privilegios

    3. Las banderas que son game over

    Hay un puñado de opciones de `docker run` que, por sí solas, anulan casi todo el aislamiento. Aparecen una y otra vez en tutoriales, en imágenes de «todo en uno» y en pipelines de CI que necesitaban que algo funcionara rápido. Reconocerlas es media batalla.

    OpciónQué hacePor qué es peligrosa
    –privilegedConcede casi todas las capabilities, desactiva seccomp/AppArmor y da acceso a todos los dispositivos del hostEscape trivial: se pueden montar discos del host, cargar módulos del kernel, acceder a /dev/mem
    -v /var/run/docker.sock:…Expone el socket de la API de Docker dentro del contenedorQuien controla el socket puede crear un contenedor nuevo con el host montado y root: equivale a root en el host
    –pid=hostEl contenedor comparte el namespace de PIDs del hostVe y puede señalizar/inspeccionar todos los procesos del host; facilita inyección y lectura de memoria ajena
    –net=hostEl contenedor usa la pila de red del host directamenteSin aislamiento de red; puede escuchar en cualquier puerto, sniffar tráfico local, saltarse reglas de firewall por interfaz
    –cap-add=SYS_ADMINAñade una capability enorme y multiusoPermite montar sistemas de ficheros, manipular namespaces y varias vías conocidas de escape
    -v /:/hostMonta el sistema de ficheros del host dentro del contenedorEscritura directa sobre binarios, cron, claves SSH o /etc/shadow del host

    El socket de Docker montado es el error más repetido

    Muchas imágenes de CI, de monitorización o de «gestión de contenedores» piden montar /var/run/docker.sock «para ver los contenedores» · Ese montaje da control total del demonio: `docker -H unix:///var/run/docker.sock run -v /:/host …` desde dentro del contenedor abre un shell con el host entero montado · Alternativas: usar la API de Docker por TCP con TLS y permisos acotados, un proxy de socket que filtre endpoints (docker-socket-proxy), o directamente Podman sin demonio

    3.1 Cómo auditar lo que ya está corriendo

    # Contenedores en modo privilegiado ahora mismo
    docker ps -q | xargs docker inspect --format \
      '{{.Name}}  privileged={{.HostConfig.Privileged}}  pid={{.HostConfig.PidMode}}  net={{.HostConfig.NetworkMode}}'
     
    # Contenedores con el socket de Docker o la raiz del host montada
    docker ps -q | xargs docker inspect \
      --format '{{.Name}}{{range .Mounts}}  {{.Source}} -> {{.Destination}}{{end}}' | grep -E 'docker.sock|-> /host| / '
     
    # Capabilities anadidas por contenedor
    docker ps -q | xargs docker inspect --format '{{.Name}}  {{.HostConfig.CapAdd}}'

    4. Linux capabilities: quitar, no añadir

    El modelo tradicional de Unix era binario: o eras root y podías todo, o no lo eras. Las capabilities trocean ese «todo» en unas 40 piezas independientes: CAP_NET_BIND_SERVICE para abrir puertos por debajo de 1024, CAP_NET_RAW para sockets crudos, CAP_SYS_TIME para cambiar el reloj, etc. Docker arranca cada contenedor con un subconjunto reducido y descarta el resto.

    El problema aparece cuando alguien añade capabilities «por si acaso». CAP_SYS_ADMIN es la más abusada: es tan amplia que se la conoce como «el nuevo root». CAP_SYS_PTRACE, CAP_SYS_MODULE, CAP_DAC_READ_SEARCH y CAP_NET_ADMIN también amplían mucho la superficie.

    PrácticaComandoEfecto
    Quitar todas y añadir solo lo necesario–cap-drop=ALL –cap-add=NET_BIND_SERVICESuperficie mínima; el patrón recomendado
    Ver qué capabilities tiene un contenedordocker exec <c> capsh –printDiagnóstico rápido de privilegios efectivos
    Evitar–cap-add=SYS_ADMIN / SYS_MODULE / SYS_PTRACECada una abre vías de escape o de interferencia con el host
    Bloquear escaladas por setuid–security-opt no-new-privilegesNi siquiera un binario setuid dentro puede subir privilegios

    Regla práctica de capabilities

    Empieza siempre por –cap-drop=ALL y añade una a una las que la aplicación necesite de verdad, comprobando qué falla · La mayoría de aplicaciones web en contenedor no necesitan ninguna capability extra si escuchan en un puerto alto · Si crees que necesitas NET_ADMIN o SYS_ADMIN, casi siempre hay una forma de rediseñar el despliegue para no necesitarla

    5. Seccomp y AppArmor: los perfiles por defecto y cómo endurecerlos

    5.1 Seccomp

    Seccomp (secure computing mode) filtra qué llamadas al sistema puede hacer un proceso. Docker aplica un perfil por defecto que bloquea alrededor de 40-50 syscalls peligrosas o inútiles dentro de un contenedor (keyctl, ptrace en ciertos contextos, add_key, syscalls de módulos del kernel, etc.). Ese perfil por defecto ya para muchas cosas; el error habitual es desactivarlo con –security-opt seccomp=unconfined para depurar y olvidarse de volver a activarlo.

    • No ejecutes con `seccomp=unconfined` en producción; si una syscall concreta falta, escribe un perfil derivado del de por defecto que solo añada esa
    • Herramientas como el generador de perfiles de contenedores o el análisis con strace en un entorno de pruebas ayudan a construir un perfil ajustado
    • –privileged también desactiva seccomp: otra razón para no usarlo

    5.2 AppArmor (o SELinux)

    En distribuciones basadas en Debian/Ubuntu, Docker carga el perfil AppArmor docker-default, que restringe escrituras en rutas sensibles, montajes y acceso a `/proc` y `/sys`. En RHEL/Fedora el equivalente es SELinux con el tipo `container_t`. Para cargas sensibles conviene un perfil propio más estricto y cargarlo con –security-opt apparmor=mi-perfil.

    # Comprobar que el perfil por defecto esta activo (no debe decir 'unconfined')
    docker inspect --format '{{.AppArmorProfile}}' <contenedor>
     
    # Cargar un perfil propio y usarlo
    sudo apparmor_parser -r -W /etc/apparmor.d/containers/mi-perfil
    docker run --security-opt apparmor=mi-perfil ...
     
    # En sistemas con SELinux: etiquetar los volumenes correctamente
    docker run -v /datos:/datos:Z ...        # :Z etiqueta el contenido para este contenedor

    6. Las imágenes: la mitad del problema

    Buena parte de los incidentes con contenedores no vienen de la configuración del runtime sino de lo que hay dentro de la imagen: librerías con CVE conocidas, binarios de más, secretos incrustados y procesos corriendo como root.

    Riesgo en la imagenMitigación
    CVE en paquetes del sistema o dependenciasEscaneo en CI con Trivy, Grype o el escáner del registro; fallar el build por severidad alta/crítica
    Superficie enorme (shell, gestor de paquetes, compiladores)Bases mínimas: distroless, Alpine, o imágenes ‘scratch’ con un binario estático
    Proceso corriendo como rootUSER no root en el Dockerfile; comprobarlo en CI
    Secretos en capas (ENV, ficheros, ARG)Nunca en la imagen: usar secretos del orquestador o montajes en runtime; escanear con git-secrets/trufflehog
    Imagen alterada entre build y despliegueFirmar con cosign (Sigstore) y verificar la firma en el clúster (policy-controller, Kyverno)
    Build multi-etapa que arrastra herramientas de compilaciónCopiar solo el artefacto final a una imagen runtime limpia
    # Escaneo de vulnerabilidades de una imagen, fallando si hay criticas
    trivy image --severity HIGH,CRITICAL --exit-code 1 miapp:1.4.2
     
    # Dockerfile: build como root, runtime como usuario sin privilegios
    FROM golang:1.22 AS build
    WORKDIR /src
    COPY . .
    RUN CGO_ENABLED=0 go build -o /app ./cmd/server
     
    FROM gcr.io/distroless/static:nonroot
    COPY --from=build /app /app
    USER nonroot:nonroot
    ENTRYPOINT ["/app"]

    Los secretos no van en la imagen ni en el ENV

    Cualquiera con la imagen puede hacer `docker history` y leer los ARG/ENV, y `docker inspect` expone las variables de entorno en runtime · Usa los secretos de Docker Swarm/Kubernetes, un gestor externo (Vault, SOPS) o montajes tmpfs; nunca capas ni variables de entorno persistentes · Para el build, `RUN –mount=type=secret` mantiene el secreto fuera de la imagen final

    7. Rootless Docker y Podman: cambiar el modelo

    Las defensas anteriores endurecen un modelo que sigue teniendo un demonio corriendo como root. Docker rootless y Podman eliminan ese demonio privilegiado: todo el stack corre bajo tu usuario, apoyándose en user namespaces. Si un contenedor escapa, aterriza como tu usuario sin privilegios, no como root.

    • Docker rootless: instalación con `dockerd-rootless-setuptool.sh`; el demonio corre en tu sesión de usuario. Limitaciones: puertos < 1024 requieren ajuste, algunas redes y almacenamiento cambian
    • Podman: sin demonio (cada contenedor es un hijo directo de tu shell), compatible con la CLI de Docker (`alias docker=podman`), integración con systemd para servicios (`podman generate systemd` / Quadlet), y pods al estilo Kubernetes
    • Buildah y Skopeo completan el ecosistema sin demonio para construir y mover imágenes
    • Para CI, ejecutar los builds rootless (o con `buildkit` en modo sin privilegios) reduce mucho el impacto de una imagen maliciosa

    Cuándo merece la pena migrar

    Servidores multiusuario, runners de CI compartidos y estaciones de desarrollo: el salto a rootless o Podman quita de la mesa la clase entera de ‘escape a root en el host’ · Clústeres gestionados (Kubernetes): el foco se mueve a Pod Security Standards, runtimes aislados (gVisor, Kata) y admission control

    8. Escapes de contenedor y cómo se detectan

    Los escapes reales suelen combinar una configuración débil con un fallo de software. A alto nivel, las familias más citadas:

    FamiliaIdea a alto nivelCondición que lo habilita
    Fugas del runtime (p. ej. CVE-2019-5736 en runc)Sobrescritura del binario del runtime desde dentro del contenedor al hacer execRuntime sin parchear; imagen o proceso controlado por el atacante
    Abuso de docker.sockCrear un contenedor nuevo con el host montado y rootSocket del demonio accesible dentro del contenedor
    –privileged / SYS_ADMINMontar dispositivos del host, abusar de cgroups (release_agent), cargar módulosContenedor privilegiado o con capabilities amplias
    Montajes sensibles del hostEscribir en /etc, cron, claves SSH o binarios del hostVolúmenes que exponen rutas del host con escritura
    Fallos del kernel compartidoElevar privilegios explotando el kernel del host desde el contenedorKernel del host vulnerable; seccomp no bloquea la syscall usada

    8.1 Detección en tiempo de ejecución

    La detección se apoya en observar el comportamiento del contenedor a nivel de syscalls y de kernel. Falco (CNCF) es la herramienta de referencia: un motor de reglas sobre eventos del kernel (vía eBPF o su módulo) que alerta de patrones anómalos. auditd cubre la parte de auditoría del sistema y eBPF —cubierto en su propio artículo— es la tecnología por debajo de casi todo el tooling moderno.

    • Shell interactivo lanzado dentro de un contenedor en producción (a menudo señal de intrusión o de operación manual no autorizada)
    • Escritura en rutas sensibles del contenedor o del host montado (`/etc`, `/proc/sysrq-trigger`, `/var/run/docker.sock`)
    • Procesos que intentan `ptrace`, cargar módulos, o montar sistemas de ficheros
    • Contenedor que abre conexiones salientes inesperadas o hacia la API de Kubernetes/Docker
    • Cambios en binarios del propio runtime o ejecución de binarios que no venían en la imagen
    # Reglas de ejemplo que trae Falco (conceptual, no configuracion completa)
    #  - 'Terminal shell in container'      -> exec de bash/sh con tty dentro de un contenedor
    #  - 'Write below etc'                  -> escritura en /etc dentro del contenedor
    #  - 'Launch Privileged Container'      -> arranque de un contenedor --privileged
    #  - 'Docker or K8s client executed'    -> uso de docker/kubectl desde dentro de un contenedor
     
    # auditd: vigilar el socket del demonio y el binario de runc en el host
    auditctl -w /var/run/docker.sock -p rwxa -k docker-socket
    auditctl -w /usr/bin/runc -p rwxa -k runc-integrity

    Checklist de endurecimiento Docker

    userns-remap activo o, mejor, Docker rootless / Podman en hosts compartidos · Cada servicio con –cap-drop=ALL y solo las capabilities imprescindibles · –security-opt no-new-privileges y sistema de ficheros –read-only por defecto · Nunca –privileged ni montar /var/run/docker.sock en contenedores de aplicación · Seccomp y AppArmor/SELinux en su perfil por defecto como mínimo; perfil propio para cargas sensibles · Imágenes mínimas (distroless/Alpine), USER no root, escaneo con Trivy/Grype en CI, firma con cosign · Secretos fuera de la imagen y del ENV: gestor externo o montajes en runtime · Runtime y kernel del host parcheados; red de contenedores segmentada y egress filtrado · Falco (o EDR equivalente) vigilando shells en contenedor, escrituras sensibles y uso de docker/kubectl

    Conclusión

    La seguridad de Docker se entiende mejor si se abandona la metáfora de la caja fuerte. Un contenedor es un proceso del host con un traje de restricciones: namespaces que le limitan la vista, cgroups que le limitan el consumo, capabilities, seccomp y AppArmor que le limitan lo que puede pedirle al kernel. Ese traje protege razonablemente bien mientras nadie le haga agujeros; el problema es que los agujeros —–privileged, el socket montado, –pid=host, correr como root— son justo lo que muchos tutoriales enseñan a hacer para que algo funcione rápido.

    El endurecimiento real es acumulativo y poco espectacular: quitar capabilities, no montar lo que no hace falta, construir imágenes mínimas sin secretos ni root, y —el cambio de mayor impacto— mover el modelo entero a rootless o Podman en cualquier host compartido. Encima de todo eso, detección en tiempo de ejecución con Falco para ver el intento antes de que el intento sea una brecha. Ninguna de estas medidas es cara; su ausencia sí.

    Referencias

    Docker docs — Security, Rootless mode, Seccomp y AppArmor profiles · CIS Docker Benchmark — lista de verificación de configuración segura del demonio y los contenedores · OWASP Docker Security Cheat Sheet — resumen práctico de controles · Falco (falco.org) y las reglas por defecto del proyecto — detección en runtime · Trivy (aquasecurity/trivy) y Grype (anchore/grype) — escaneo de imágenes · Sigstore / cosign — firma y verificación de imágenes · NIST SP 800-190 — Application Container Security Guide · Nuestros artículos sobre contenedores y sobre eBPF, para el contexto de base

    unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

    Resumen de privacidad

    Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.