Seguridad del socket de Docker: el montaje que equivale a root (y alternativas más seguras)

¿Te fue útil?

En algún lugar de tu flota, un contenedor tiene -v /var/run/docker.sock:/var/run/docker.sock porque “necesita construir imágenes”
o “necesita inspeccionar contenedores.” Funciona. Se despliega. Todo el mundo deja de pensarlo.

Hasta el día en que una cuenta de servicio aparentemente inocua dentro de ese contenedor descubre que puede iniciar contenedores privilegiados, montar el sistema de archivos del host
y reescribir tu realidad. Un montaje de volumen. Control a nivel de host. No es un “tal vez” teórico. Es una vía de escalada práctica y repetible.

El problema del “un montaje”: por qué docker.sock es básicamente root

El daemon de Docker (dockerd) es un proceso privilegiado de larga ejecución en el host. Puede:
crear namespaces, configurar cgroups, gestionar redes, montar sistemas de archivos e iniciar contenedores con amplios privilegios. Ese es su trabajo.
La CLI de Docker es solo un cliente que envía llamadas API al daemon.

/var/run/docker.sock es el socket Unix donde vive esa API por defecto. Si un proceso puede hablar con el socket con
permisos suficientes, puede pedir al daemon que haga cosas a nivel de host en su nombre. El daemon no sabe (ni le importa) si la solicitud
vino de “un administrador de confianza en la terminal” o de “una app Node.js dentro de un contenedor.” Simplemente recibe llamadas API.

Este es el meollo: cuando montas el socket Docker del host dentro de un contenedor, efectivamente le estás dando a ese contenedor la capacidad de controlar
el runtime de contenedores del host. Eso es más poder del que normalmente tiene el contenedor por sí mismo.

En la práctica, significa que un contenedor comprometido puede:

  • Iniciar un nuevo contenedor en modo --privileged.
  • Montar el sistema de archivos del host en ese contenedor (por ejemplo, /:/host).
  • Escribir en rutas del host, alterar configuraciones, dejar claves SSH, leer secretos o modificar unidades systemd.
  • Configurar la red para olfatear o redirigir tráfico.
  • Descargar y ejecutar imágenes arbitrarias como vector de ejecución.

Si eso suena a “root”, es porque lo es, solo con una interfaz de usuario ligeramente diferente.
El socket no es “una cosa de Docker”. Es una capacidad equivalente a root.

Primera broma (y vamos a mantener las bromas con correa corta): Montar docker.sock en un contenedor es como repartir llaves maestras—excepto que las llaves también pueden construir un edificio nuevo.

“Pero solo lo usamos para builds” no es una mitigación

La API de Docker no es granular por defecto. Si puedes crear contenedores, puedes crear contenedores que monten el host. Si puedes montar el host,
puedes hacer todo. La API no se detiene educadamente en “solo construir, nada de travesuras.”

El límite de seguridad aquí no es el contenedor; es el daemon. Y el daemon está en el host con privilegios de host.
Así que la pregunta se convierte en: “¿Quién puede pedirle al daemon que haga cosas?” Si la respuesta es “cualquier proceso en este contenedor,”
has ampliado tu radio de impacto para incluir cada bug en la cadena de dependencias de ese contenedor.

Algunos datos e historia que explican cómo llegamos aquí

Esto no son trivialidades por trivialidad. Explican por qué los patrones de socket de Docker son tan comunes, por qué perduran y por qué existen alternativas modernas.

  1. Docker nació como una herramienta de conveniencia para desarrolladores. La adopción temprana de Docker priorizó “funciona en mi máquina” más que el rigor en control de acceso.
  2. La separación daemon/cliente siempre estuvo ahí. Incluso la clásica CLI docker ha sido un cliente remoto; el valor por defecto simplemente es un socket local.
  3. El grupo docker históricamente equivale a root. En muchos sistemas Linux, el acceso a /var/run/docker.sock se concede por pertenecer al grupo docker, lo que efectivamente permite acciones a nivel root.
  4. Se eligieron sockets Unix por ergonomía local. Son rápidos, simples y evitan exponer un puerto TCP por defecto—pero no resuelven la autorización.
  5. Docker remoto sobre TCP existió pronto, y a menudo se configuró mal. “Abrir el 2375 al mundo” se volvió un patrón recurrente de incidentes en los 2010s.
  6. Las herramientas de build evolucionaron porque el patrón de socket era doloroso. BuildKit, builds rootless y builders “sin daemon” ganaron adopción en parte para evitar dar control del host a runners de CI.
  7. Docker-in-Docker (DinD) se convirtió en un parche con sus propias aristas. Redujo compartir el socket del host pero introdujo necesidades de privilegios, complejidad de almacenamiento y límites de aislamiento anidado.
  8. Kubernetes hizo que el problema escalara. Una vez que montas un socket de runtime en Pods (Docker, containerd, sockets CRI), “un Pod malo” puede convertirse en “un nodo malo.”

La lección histórica: la mayor parte de las exposiciones de sockets no son maliciosas; son conveniencias accidentales que se endurecieron hasta volverse “práctica estándar.”
En producción, “práctica estándar” es lo que se audita.

Modelo de amenazas: qué puede hacer un atacante con el socket

Supón que el atacante tiene ejecución de código dentro de un contenedor que tiene montado el socket docker del host. Esto puede ocurrir vía:
RCE en tu app, una dependencia maliciosa, un job de CI comprometido o un endpoint de administración expuesto. ¿Y ahora qué?

Ruta de escalada en pasos llanos

El atacante puede ejecutar la CLI de Docker si existe en el contenedor, o puede usar HTTP crudo sobre el socket Unix.
De cualquier forma, puede solicitar:

  • Crear un contenedor con --privileged (o un conjunto objetivo de capacidades).
  • Montar / del host dentro del contenedor.
  • Hacer chroot en el sistema de archivos del host y modificarlo.
  • Persistir: servicio systemd, cron, claves SSH, authorized_keys o reemplazar binarios.

No es solo “root”; también es “plano de control”

Incluso sin montar /, controlar Docker puede:

  • Detener o reiniciar servicios críticos.
  • Leer variables de entorno de otros contenedores (a menudo incluyendo tokens).
  • Adjuntarse a contenedores en ejecución y exfiltrar secretos en memoria.
  • Crear pivotes de red al unirse a redes de contenedores.
  • Descargar imágenes desde registries usando las credenciales del host.

“Pero el contenedor no es privilegiado” es un malentendido

El contenedor mismo puede no ser privilegiado. No importa. El daemon es privilegiado. Estás pidiendo a un proceso privilegiado del host que haga trabajo privilegiado del host.
El límite no lo aplica el daemon a menos que lo configures para que lo haga.

Aquí hay un encuadre operativamente útil: docker.sock es una interfaz de administrador. Móntalo solo allí donde también concederías un shell root en el host.
Si esa frase te pone incómodo, bien—ahora podemos arreglarlo.

Guía de diagnóstico rápido

Cuando sospeches exposición del socket docker (o estás respondiendo a un “¿por qué este contenedor puede hacer eso?”), la rapidez importa. Revisa en este orden:

Primero: ¿está montado o accesible el socket?

  • Inspecciona los mounts del contenedor buscando /var/run/docker.sock.
  • Comprueba el sistema de archivos por la ruta del socket y sus permisos.
  • Confirma si el proceso puede hablar con él (una llamada API fallida sigue siendo un dato).

Segundo: ¿quién puede acceder?

  • Propiedad y grupo del socket; revisa quién está en el grupo docker.
  • Dentro del contenedor: ¿el proceso corre como root? ¿Está en un grupo mapeado al socket?
  • ¿Hay sidecars o agentes con permisos más amplios?

Tercero: ¿qué puede hacer ahora mismo?

  • Intenta una llamada de solo lectura (docker ps) y una de escritura (docker run) de manera controlada.
  • Revisa la configuración del daemon: ¿TLS? ¿plugins de autorización? ¿daemon rootless? ¿remapeo de user namespace?
  • Busca runners de CI, herramientas de despliegue o contenedores “de monitorización” que silenciosamente lleven el socket.

Cuarto: comprobaciones de radio de impacto y persistencia

  • Busca contenedores iniciados con --privileged, montajes al host o namespaces host PID/network.
  • Audita eventos recientes de inicio de contenedores; revisa imágenes no familiares.
  • Revisa el host por nuevas unidades systemd, trabajos cron, claves SSH o binarios modificados.

Este playbook es intencionalmente directo. El objetivo es identificar si tratas con “normal pero riesgoso” o “explotado activamente.”
Puedes refinar después. Ahora mismo quieres verdad en el terreno.

Tareas prácticas: comandos, salidas y decisiones

Estas son tareas reales que puedes ejecutar en un host Linux o en un contenedor (cuando corresponda). Cada una incluye:
comando, salida de ejemplo, qué significa la salida y la decisión que tomas a partir de ella.

Tarea 1: Verificar si existe el socket de Docker y cómo está protegido

cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan  3 09:12 /var/run/docker.sock

Significado: Es un socket Unix (s) propiedad de root y grupo docker, grabable por el grupo.

Decisión: Trata la membresía en docker como acceso privilegiado. Si los contenedores pueden mapearse a ese grupo, tienes un problema de límite de privilegios.

Tarea 2: Listar miembros del grupo docker (lado host)

cr0x@server:~$ getent group docker
docker:x:998:jenkins,deploy,alice

Significado: Esos usuarios probablemente pueden controlar Docker en el host.

Decisión: Reduce esa lista agresivamente. Si “deploy” es una cuenta compartida, esencialmente están compartiendo root.

Tarea 3: Confirmar con qué daemon habla tu CLI

cr0x@server:~$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT               ERROR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock

Significado: Tu contexto por defecto es el socket local propiedad de root.

Decisión: Si esperabas un builder remoto o un daemon sin root, no lo estás usando. Arregla el flujo de trabajo, no la narrativa.

Tarea 4: Encontrar contenedores que montan el socket de Docker

cr0x@server:~$ docker ps --format '{{.ID}} {{.Names}}' | while read id name; do docker inspect -f '{{.Name}} {{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' "$id"; done | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock -> /var/run/docker.sock
/portainer /var/run/docker.sock -> /var/run/docker.sock

Significado: Dos contenedores tienen control Docker del host. El runner de CI es esperado; Portainer puede ser aceptable, pero ambos son objetivos de alto valor.

Decisión: Para cada contenedor: justifícalo, encáusaló o elimínalo. “Siempre lo hicimos” no es una justificación.

Tarea 5: Dentro de un contenedor sospechoso, probar si el socket es usable

cr0x@server:~$ docker exec -it ci-runner sh -lc 'id && ls -l /var/run/docker.sock'
uid=1000 gid=1000 groups=1000
srw-rw---- 1 root docker 0 Jan  3 09:12 /var/run/docker.sock

Significado: El proceso no está en el grupo docker. Eso podría bloquear el acceso—a menos que el contenedor se ejecute como root a veces, o el ID de grupo se mapea diferente.

Decisión: Intenta una llamada API inofensiva a continuación. No asumas que estás seguro porque id parece sin privilegios.

Tarea 6: Intentar una llamada Docker de solo lectura desde dentro

cr0x@server:~$ docker exec -it ci-runner sh -lc 'docker ps'
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json": dial unix /var/run/docker.sock: connect: permission denied

Significado: El socket está montado, pero el usuario actual no puede acceder.

Decisión: Esto sigue siendo un riesgo: si el contenedor puede volverse root (malconfiguración, SUID, exploit), todo está comprometido. También revisa si el contenedor alguna vez corre como root durante jobs.

Tarea 7: Comprobar si el contenedor se ejecuta como root (lado host)

cr0x@server:~$ docker inspect -f '{{.Name}} user={{.Config.User}}' ci-runner
/ci-runner user=

Significado: Usuario vacío a menudo significa root por defecto dentro del contenedor.

Decisión: Si este contenedor además tiene el socket, trátalo como “root en host, esperando a ocurrir.” Arregla ahora: ejecuta como non-root y elimina el socket, o aísla el daemon.

Tarea 8: Probar la escalada (en un laboratorio controlado), montando la raíz del host

cr0x@server:~$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock alpine sh -lc 'apk add --no-cache docker-cli >/dev/null && docker run --rm -it --privileged -v /:/host alpine sh -lc "ls -l /host/etc/shadow | head -n 1"'
-rw-r-----    1 root     shadow        1251 Jan  3 08:59 /host/etc/shadow

Significado: Un contenedor con acceso al socket creó un contenedor privilegiado que puede leer archivos sensibles del host.

Decisión: Si tú puedes hacer esto, también lo puede hacer un atacante. Elimina los mounts de socket en cargas de trabajo de propósito general.

Tarea 9: Identificar contenedores privilegiados y montajes al host

cr0x@server:~$ docker ps -q | xargs -r docker inspect -f '{{.Name}} privileged={{.HostConfig.Privileged}} mounts={{range .Mounts}}{{.Source}}:{{.Destination}},{{end}}'
/ci-runner privileged=false mounts=/var/run/docker.sock:/var/run/docker.sock,
/node-exporter privileged=false mounts=/proc:/host/proc,/sys:/host/sys,
/debug-shell privileged=true mounts=/:/host,

Significado: /debug-shell es privilegiado y monta la raíz del host. Eso es una palanca de emergencia—aceptable si está controlado, catastrófico si se olvida.

Decisión: Elimina o restringe los contenedores “debug”. Aplica políticas que prevengan contenedores privilegiados + montajes al host fuera de flujos de trabajo de emergencia.

Tarea 10: Comprobar la configuración de escucha del daemon (evitar exposición TCP accidental)

cr0x@server:~$ ps aux | grep -E 'dockerd|docker daemon' | grep -v grep
root      1321  0.3  1.4 1332456 118320 ?      Ssl  08:58   0:06 /usr/bin/dockerd -H fd://

Significado: Usa activación por socket de systemd (-H fd://), no escucha explícitamente en TCP.

Decisión: Bien. Si ves -H tcp://0.0.0.0:2375 sin TLS, trátalo como incidente.

Tarea 11: Verificar si la API de Docker es alcanzable en la red

cr0x@server:~$ ss -lntp | grep -E '(:2375|:2376)\b' || true

Significado: No hay listeners en los puertos TCP comunes de Docker.

Decisión: Manténlo así a menos que tengas TLS y una historia de autorización. “Lo pondremos en un firewall después” es como terminas en una llamada el lunes.

Tarea 12: Consultar Docker vía socket crudo (útil cuando no está disponible la CLI docker)

cr0x@server:~$ curl --unix-socket /var/run/docker.sock http://localhost/version
{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"26.0.0","Details":{"ApiVersion":"1.45","GitCommit":"...","GoVersion":"...","Os":"linux","Arch":"amd64","KernelVersion":"6.5.0"}}],"Version":"26.0.0","ApiVersion":"1.45","MinAPIVersion":"1.24","GitCommit":"...","GoVersion":"...","Os":"linux","Arch":"amd64","KernelVersion":"6.5.0","BuildTime":"..."}

Significado: Si esto tiene éxito dentro de un contenedor, ese contenedor tiene acceso de administrador a Docker incluso si no trae la CLI.

Decisión: No dejes que “no instalamos docker” te engañe. El acceso es sobre el socket, no sobre el binario.

Tarea 13: Auditar la unidad systemd de Docker por banderas (lado host)

cr0x@server:~$ systemctl cat docker | sed -n '1,120p'
# /lib/systemd/system/docker.service
[Service]
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Significado: No hay un listener TCP inseguro explícito configurado aquí.

Decisión: Si debes exponer TCP, hazlo explícitamente con TLS y restringe clientes; de lo contrario mantenlo local.

Tarea 14: Identificar qué imágenes y contenedores son más propensos a ser explotados

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | sed -n '1,15p'
NAMES         IMAGE                 STATUS
ci-runner     company/runner:latest Up 3 hours
portainer     portainer/portainer   Up 7 days
api           company/api:2026.01   Up 2 days

Significado: “Runner” y contenedores de “UI admin” son de alto valor, con frecuencia expuestos a Internet y a menudo complejos.

Decisión: Prioriza eliminar mounts de socket de todo lo que procese entrada no confiable o ejecute jobs de terceros.

Tarea 15: Si usas Docker rootless, confírmalo

cr0x@server:~$ docker info --format 'rootless={{.SecurityOptions}}'
rootless=[name=seccomp, name=rootless]

Significado: El daemon se ejecuta en modo rootless (o al menos reporta la opción de seguridad rootless).

Decisión: Rootless reduce el riesgo de takeover del host, pero no lo borra. Evalúa qué puede acceder el daemon rootless (rutas de almacenamiento, credenciales, red).

Tarea 16: Detectar “docker.sock con otro nombre” (sockets containerd / CRI)

cr0x@server:~$ ls -l /run/containerd/containerd.sock 2>/dev/null || true
srw-rw---- 1 root root 0 Jan  3 08:58 /run/containerd/containerd.sock

Significado: Existe el socket de containerd. Montar esto (o sockets CRI) en un contenedor puede exponer control a nivel de nodo de forma similar, dependiendo de qué esté escuchando y cómo esté protegido.

Decisión: No juegues al gato y al ratón con nombres de archivo. Trata los sockets de control del runtime como interfaces privilegiadas en general.

Tres mini-historias corporativas desde el terreno

Mini-historia 1: El incidente causado por una suposición incorrecta

Una empresa mediana tenía un contenedor “utilitario” corriendo en cada host de build. Recogía logs de build, subía artefactos y—porque era conveniente—
tenía montado el socket Docker para poder etiquetar contenedores y limpiar imágenes antiguas.

Alguien supuso que el contenedor era de bajo riesgo porque “solo hablaba con sistemas internos.” El contenedor también corría un pequeño endpoint HTTP para chequeos de salud
y métricas. El endpoint aceptaba algunos parámetros y los escribía en logs. Un pequeño bug de inyección se coló durante un refactor apresurado.

Un escáner aburrido encontró el endpoint. En minutos, el atacante tenía ejecución remota de código dentro del contenedor utilitario. El atacante no necesitó un exploit de kernel.
Simplemente usó el socket para lanzar un contenedor privilegiado que montó el sistema de archivos del host, y luego dejó un binario backdoor en una ruta usada por un cron.

La detección fue embarazosa: picos de CPU, reinicios de contenedores y una oleada de “¿por qué este host hace conexiones salientes?” El equipo inicialmente
persiguió problemas de red y timeouts al registry. La causa raíz resultó ser el truco más viejo del libro de Docker: un contenedor controlando Docker.

La solución no fue “parchar la inyección.” Eliminaron mounts de socket de todo lo que no fuera un componente de build estrictamente controlado, y movieron la limpieza a un
timer systemd a nivel host ejecutado bajo propiedad administrativa explícita. Al equipo de seguridad no le encantó, pero al menos tenían un radio de impacto conocido.

Mini-historia 2: La optimización que salió mal

Otra organización tenía cargas de CI pesadas. Los builds eran lentos, sobre todo porque tirar imágenes base y capas saturaba el registry y la cache estaba fría.
Alguien tuvo una optimización ingeniosa: ejecutar un “servicio de cache de builds” compartido en cada nodo, montar docker.sock y que precargara imágenes y calentara la cache.
Redujo el tiempo de build. La gente aplaudió. El cambio pasó porque era “solo rendimiento.”

El problema llegó silencioso. El servicio de cache necesitaba permisos amplios para gestionar imágenes entre proyectos. Los jobs de CI empezaron a depender indirectamente de él, y pronto
el servicio de cache se volvió de facto un plano de control para los hosts de build. También se volvió una dependencia. Cuando falló, los builds se detuvieron.

Luego vino la revisión de seguridad. Los revisores hicieron una pregunta aburrida: “Si un job de CI se compromete, ¿puede llegar al contenedor de cache?”
La respuesta fue sí—misma red, mismo nodo, variables de entorno compartidas y muchas oportunidades de movimiento lateral.

El montaje de socket significó que cualquier compromiso del contenedor de cache era un compromiso del host. Pero era peor: el servicio de cache descargaba imágenes con las credenciales del host
hacia el registry. Si puedes descargar con esas credenciales, también puedes exfiltrarlas de varias maneras divertidas. La optimización no solo aumentó la superficie de ataque;
la centralizó.

Retrocedieron y lo reemplazaron por un builder BuildKit remoto con credenciales acotadas, más cache en el lado del registry. Los builds quedaron algo más lentos que la
solución “ingeniosa” máxima, pero el riesgo se desplomó. El rendimiento es una característica; la supervivencia es el producto.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una gran empresa tenía una política simple: nada de mounts de docker.sock en cargas de trabajo de aplicación. Las excepciones requerían un formulario corto y un plan de controles compensatorios.
La gente refunfuñaba. Por supuesto que lo hacía. La política se aplicaba mediante guías de revisión de imágenes y auditorías periódicas de contenedores en ejecución.

Un día, un nuevo equipo desplegó un contenedor agente de un proveedor “para ayudar con la observabilidad.” El quickstart del proveedor usaba un montaje de socket para descubrir contenedores.
El equipo pidió una excepción, como era de esperar. Seguridad pidió una alternativa de solo lectura y un alcance razonado. El proveedor dijo “es seguro.”
Seguridad dijo “muéstranos.”

Durante las pruebas, un ingeniero demostró que el agente podía crear contenedores y montar el host. Eso terminó la discusión de “seguro.”
El equipo desplegó en su lugar el agente con una fuente de datos reducida (métricas de cgroup y procfs) y un colector a nivel host separado para metadata de contenedores.

Un mes después, una vulnerabilidad en ese agente proveedor salió en las noticias. La parchearon según lo programado, pero la vulnerabilidad nunca se convirtió en takeover del host
porque el agente nunca tuvo control del host. El resultado no fue dramático. Ese es el punto. Los controles aburridos suelen ser los que mantienen corto tu informe de incidentes.

Alternativas más seguras (con compensaciones, no cuentos)

La sustitución correcta depende de por qué montaste el socket en primer lugar. “Necesitamos Docker en un contenedor” no es un requisito; es un síntoma.
A continuación hay patrones que funcionan en sistemas reales, incluyendo lo que te cuestan.

1) No lo hagas: mueve las operaciones de Docker al host (timers systemd, scripts controlados)

Si la única razón para acceso al socket es limpieza, pruning de imágenes, rotación de logs o recopilación de metadata—hazlo en el host.
Usa un timer systemd o cron con propiedad explícita y auditoría.

Compensaciones: Un poco más de gestión del host. Pero recuperas límites claros de privilegios y reduces el número de procesos que pueden dirigir el daemon.

2) Daemon Docker rootless (o BuildKit rootless) para workloads no confiables

Docker rootless ejecuta el daemon sin privilegios root. Esto cambia el radio de impacto: el acceso a la API de Docker ya no equivale automáticamente a root del host.
Sigue siendo poderoso—solo que limitado por los permisos del usuario.

Compensaciones: Algunas funcionalidades de red, contenedores privilegiados y capacidades a nivel kernel no funcionarán. El rendimiento y la compatibilidad pueden variar.
Operativamente, ahora gestionas daemons por usuario, rutas de almacenamiento y ubicaciones de sockets.

3) Builders remotos: BuildKit como servicio (dominio de seguridad separado)

Un caso común de “necesitamos docker.sock” son builds de imágenes en CI. El patrón más seguro: CI habla con un builder remoto que está aislado, endurecido y acotado.
Tus workloads de aplicación nunca ven un socket de runtime. Tus runners de CI no obtienen derechos de administrador del host.

Compensaciones: Requiere conectividad de red y gestión de credenciales. Depurar builds puede pasar de “ssh al host” a “inspeccionar logs del builder.”
Vale la pena.

4) Builds sin daemon: enfoques tipo Kaniko

Los builders sin daemon pueden crear imágenes de contenedor sin requerir privilegios del daemon de Docker. Eso elimina el incentivo de montar docker.sock en jobs de CI.

Compensaciones: No todas las características de Dockerfile se comportan idénticamente. El cache de capas y el rendimiento pueden variar. Cambias privilegios del daemon por peculiaridades de la herramienta de build.

5) Docker-in-Docker (DinD): mejor que montar el socket, pero con aristas

DinD ejecuta un daemon Docker dentro de un contenedor. Los jobs de CI hablan con ese daemon interno, no con el daemon del host. Esto puede reducir el riesgo de takeover del host por código de job.
Pero en la práctica DinD a menudo necesita --privileged para funcionar bien, y el aislamiento de almacenamiento se complica rápido.

Compensaciones: Riesgo de contenedor privilegiado, complejidad de cgroups anidados, costos de rendimiento y tendencia a convertirse en “infraestructura temporal” que nunca se retira.

6) Plugins de autorización y enforcement de políticas (cuando realmente debes exponer el socket)

Si hay una necesidad legítima de acceso controlado a la API de Docker, puedes añadir capas de política:
plugins de autorización, TLS con certificados cliente para acceso remoto y controles estrictos sobre qué endpoints API están permitidos.

Compensaciones: Ahora ejecutas y mantienes una capa de auth sobre tu runtime de contenedores. Si falla abierta, pierdes seguridad. Si falla cerrada, te despiertan a las 3 AM.
Aun así, es mejor que confiar en la esperanza.

7) Reemplaza la “introspección Docker” por señales de solo lectura

Los agentes de monitoreo suelen pedir el socket para enumerar contenedores y labels. Alternativas:
leer /proc, cgroups, exportadores node, o drivers de logs; aceptar menos metadata; o ejecutar un colector de confianza a nivel host que exporte métricas saneadas.

Compensaciones: Menor fidelidad. Pero para la mayoría de casos de monitoreo, “menos metadata” supera a “root por accidente.”

8) Si estás en Kubernetes: hazlo cumplir con controles de admisión y reemplazos de PSP

Si corres Kubernetes, deja de confiar solo en revisiones. Añade políticas para denegar mounts de sockets de runtime y hostPath salvo permiso explícito.
También bloquea pods privilegiados y hostPID/hostNetwork excepto para componentes de sistema conocidos.

Compensaciones: Romperás el “pod de debug rápido” de alguien. Está bien. Proporciona un namespace de emergencia con RBAC fuerte y logs de auditoría.

Una cita para mantenerte honesto

La esperanza no es una estrategia. — General Gordon R. Sullivan

No tienes que ser paranoico. Solo debes ser realista sobre lo que montaste.

Segunda broma (última, lo prometo): El socket de Docker es como una regla de firewall “temporal”—todos recuerdan que existe justo después del incidente.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: “Nuestro contenedor de app puede iniciar otros contenedores”

Causa raíz: Socket docker del host montado en el contenedor de la app, o el contenedor tiene acceso a permisos del grupo docker.

Solución: Elimina el montaje del socket; rediseña el flujo (builder remoto, automatización a nivel host). Asegura que el contenedor se ejecute como non-root y no tenga acceso al grupo docker.

2) Síntoma: “Quitamos la CLI docker pero el contenedor sigue controlando Docker”

Causa raíz: La API es accesible vía el socket; herramientas como curl pueden hablar con ella directamente.

Solución: Elimina acceso al socket. Seguridad no es “ausencia de un binario cliente.”

3) Síntoma: “Solo CI tiene docker.sock, así que estamos bien”

Causa raíz: CI ejecuta código no confiable (PRs, dependencias, scripts de build). Justamente ahí no quieres admin del host.

Solución: Usa builders remotos o builds sin daemon. Si debes usar un daemon, aíslalo por job y restringe credenciales.

4) Síntoma: “Contenedores aleatorios corren privilegiados y nadie sabe por qué”

Causa raíz: Un contenedor con acceso al socket los inició, o un flujo “debug” se normalizó en producción.

Solución: Audita fuentes de inicio de contenedores; elimina mounts de socket; aplica políticas que nieguen contenedores privilegiados fuera de namespaces controlados.

5) Síntoma: “Expusimos Docker por TCP por conveniencia”

Causa raíz: dockerd escuchando en tcp://0.0.0.0:2375 (a menudo sin TLS), o reglas de firewall demasiado permisivas.

Solución: Deshabilita el listener TCP; si requieres acceso remoto, usa TLS en 2376 con certificados cliente y ACLs de red estrictas, además de controles de autorización.

6) Síntoma: “El agente de monitoreo exige docker.sock, el proveedor dice que es obligatorio”

Causa raíz: Default de conveniencia del proveedor. Quieren metadata completa; piden la interfaz más fácil.

Solución: Usa un colector a nivel host; proporciona fuentes de solo lectura; negocia alcance reducido; rehusa el montaje del socket en agentes de propósito general.

7) Síntoma: “Probamos rootless, pero los builds fallaron”

Causa raíz: Algunas características de Dockerfile y operaciones privilegiadas esperan comportamiento con root, o tu pipeline asume networking directo del host.

Solución: Usa un builder BuildKit remoto con privilegios controlados; separa “build” de “run”; ajusta Dockerfiles; acepta que algunas suposiciones heredadas deben morir.

8) Síntoma: “Usamos DinD y ahora el uso de disco es enorme”

Causa raíz: Almacenamiento de daemon anidado en overlay2 dentro del filesystem del contenedor; caches se acumulan por runner; pruning no afecta al host como se espera.

Solución: Usa volúmenes externos para cache del builder con gestión de ciclo de vida; o cambia a builders remotos/cache en registry; implementa políticas explícitas de prune por entorno.

Listas de verificación / plan paso a paso

Paso a paso: eliminar montajes inseguros de docker.sock sin romper producción

  1. Inventario del uso del socket.

    • Lista contenedores en ejecución que montan /var/run/docker.sock.
    • Lista manifests/archivos compose que lo incluyen.
    • Clasifícalos por propósito: builds CI, monitorización, UI admin, limpieza, “varios”.
  2. Decide cuáles son prohibiciones totales.

    • Cargas de aplicación que procesan entrada no confiable: prohibir.
    • Agentes de terceros: prohibición por defecto.
    • Runners de CI: alto escrutinio; probablemente rediseño.
  3. Sustituye primero los casos de “acceso a metadata”.

    • Cambia la introspección por socket por métricas de cgroup/procfs cuando sea posible.
    • Usa colectores a nivel host para el resto.
  4. Arregla builds CI a continuación (mayor riesgo).

    • Elige un patrón de builder: BuildKit remoto, builds sin daemon o DinD aislado por job.
    • Acota credenciales de registry al mínimo necesario por repositorio.
    • Separa secretos de build de secretos de runtime.
  5. Reforza las excepciones que queden.

    • Ejecuta como non-root.
    • Usa aislamiento de red y reglas de entrada estrictas.
    • Añade logging de auditoría para llamadas a la API de Docker si es factible.
    • Documenta la justificación y una fecha de eliminación.
  6. Haz cumplir, no rogar.

    • Añade chequeos de política en CI (rechaza manifests compose/k8s con mounts de socket).
    • Añade auditorías de tiempo de ejecución y alertas (escaneo periódico de contenedores en ejecución).

Lista operativa: si debes permitir acceso al socket (raro)

  • El contenedor con acceso al socket se trata como un agente privilegiado del host, no como una app regular.
  • La imagen del contenedor es mínima, fija y parcheada agresivamente.
  • Se ejecuta como non-root cuando sea posible; sin shell, sin gestor de paquetes en imágenes de producción.
  • La exposición de red se minimiza; sin entrada desde redes no confiables.
  • No hay secretos compartidos que permitan movimiento lateral (tokens acotados; credenciales de corta vida).
  • Logging sólido: eventos de inicio de contenedor, pulls de imágenes y eventos del daemon se monitorean.
  • Propiedad clara: quién recibe la alerta si se comporta mal y quién aprueba cambios.

Preguntas frecuentes

1) ¿Montar /var/run/docker.sock siempre equivale a root?

En configuraciones Docker con root, prácticamente sí. Si un proceso puede emitir llamadas API de Docker que crean contenedores, normalmente puede escalar a control total del host
iniciando un contenedor privilegiado y montando el filesystem del host. Hay casos límite (autorización personalizada, daemons restringidos), pero no apuestes tu flota a casos límite.

2) ¿Y si el socket se monta en solo lectura?

Un montaje en solo lectura afecta operaciones de escritura en el nodo del socket, no la capacidad de enviar peticiones API sobre él. Si puedes abrir el socket, puedes hablar con él.
El “montaje docker.sock de solo lectura” es mayormente teatro de seguridad.

3) ¿Y si el contenedor corre como non-root?

Mejor, pero no suficiente. Si el usuario non-root puede acceder al socket (vía mapeo de grupos o permisos permisivos), sigues expuesto.
Incluso si no puede hoy, escapes de contenedor y escaladas dentro del contenedor se vuelven mucho más valiosas cuando el socket está presente.

4) ¿No está Docker ya aislado por namespaces?

Los contenedores están aislados del host por namespaces y cgroups. El daemon de Docker no es un contenedor; es un proceso del host con privilegios de host.
Dar acceso al código en contenedores al daemon es como darle una API de administrador a tu capa de aislamiento.

5) Nuestro proveedor requiere docker.sock para monitoreo. ¿Cuál es la alternativa pragmática?

Ejecuta un colector a nivel host (como servicio del sistema) para recopilar metadata de contenedores y exportar métricas saneadas.
O acepta metadata reducida vía cgroup/procfs. Si el proveedor insiste en que el socket es obligatorio, trata su agente como un componente privilegiado y aísla en consecuencia.

6) ¿Exponer Docker por TCP con TLS es más seguro que montar el socket?

Puede serlo, si realmente haces cumplir la autenticación de clientes (mutual TLS), restringes el acceso de red y idealmente aplicas políticas de autorización.
También es más fácil configurarlo mal por accidente. Un socket local al menos no es alcanzable desde Internet por defecto.

7) ¿Docker rootless resuelve totalmente el problema?

Rootless reduce el aspecto de “root del host” porque el daemon corre como un usuario sin privilegios. Pero no convierte la exposición de la API de Docker en algo inofensivo.
Un atacante todavía puede controlar builds, descargar imágenes, exfiltrar credenciales disponibles para ese usuario y afectar el servicio. Es una mitigación, no una carta blanca.

8) ¿Cuál es el enfoque más limpio para builds de imágenes en CI hoy?

Un builder BuildKit remoto o un builder sin daemon suele ser lo más limpio: los jobs de CI envían builds, obtienen artefactos y nunca obtienen control de runtime a nivel host.
La elección exacta depende de tus necesidades de cache, características de Dockerfile y gestión de secretos.

9) ¿Cómo convenzo a stakeholders que solo quieren entregar rápido?

Enfócalo como un asunto de seguridad en producción: un montaje de socket convierte cualquier RCE en la app en compromiso del host. Eso cambia severidad del incidente, tiempo de recuperación
y exposición de cumplimiento. Ofrece un plan de migración con hitos medibles (inventario, reemplazo de monitorización, reemplazo de builds CI, aplicación de políticas).

10) Si quitamos el socket, ¿cómo hacemos limpieza y pruning de contenedores?

Ejecuta la limpieza en el host vía timers systemd o con la gestión de ciclo de vida del orquestador. Si debes hacerlo desde un contenedor, ejecuta un agente host dedicado y endurecido
con una excepción documentada y aislamiento de red estricto. Pero preferible: limpieza gestionada por el host.

Conclusión: pasos prácticos siguientes

El socket de Docker es una interfaz de administrador. Trátalo como tal. Si tu postura por defecto es “montarlo donde sea,” has construido una vía de escalada de privilegios en tu plataforma.
La solución no es una bandera única; es una decisión: separar “workloads” de “planos de control.”

Pasos que puedes dar esta semana:

  1. Inventaria cada montaje de socket y cada usuario en el grupo docker.
  2. Elimina montajes de socket de cargas de trabajo de aplicación primero. Sin debate, sin excepciones por defecto.
  3. Refactoriza builds CI para usar builders remotos o builders sin daemon.
  4. Reemplaza el uso del socket en monitorización por colectores host o señales de solo lectura.
  5. Aplica con comprobaciones de política y auditorías en tiempo de ejecución para que esto no vuelva en seis meses.

Los sistemas en producción no fallan porque alguien no supiera. Fallan porque una conveniencia riesgosa se volvió invisible.
Hazla visible. Luego bórrala.

← Anterior
Docker: reglas de enrutamiento de Traefik que fallan silenciosamente — corrige las etiquetas correctamente
Siguiente →
Ubuntu 24.04: Swap en SSD — hazlo de forma segura (y cuándo no deberías) (caso #50)

Deja un comentario