Si alguna vez has escrito docker ps en un servidor y te sentiste como un adulto responsable, aquí viene un giro:
ese mismo poder puede ser accesible desde Internet abierto con un cambio de configuración descuidado. Sin cadena de exploits. Sin payload sofisticado.
Solo “hola, daemon” y tu host se convierte en la potencia de cómputo de otra persona.
La API remota de Docker no es “solo otro puerto de servicio”. Es un plano de control cercano a root que puede iniciar contenedores privilegiados,
montar el sistema de archivos del host y persistir silenciosamente. Trátala como SSH con autenticación por contraseña y una IP pública — porque funcionalmente es peor.
Por qué la API remota de Docker es básicamente root
El daemon de Docker (dockerd) es la autoridad que crea namespaces, configura cgroups, monta sistemas de archivos,
gestiona la red de contenedores y arranca procesos en tu nombre. Cuando controlas el daemon, controlas el host.
La API remota (sobre un socket Unix o TCP) no es una “interfaz de gestión” como un panel de solo lectura. Es el volante.
Aquí está el problema central: el daemon realiza acciones privilegiadas y confía en las solicitudes que llegan a su endpoint de API.
Si ese endpoint es accesible y no está fuertemente autenticado, cualquiera que pueda comunicarse con él puede:
- Iniciar un contenedor con
--privileged - Hacer un bind-mount de
/del host dentro de un contenedor - Escribir claves SSH en
/root/.ssh/authorized_keys - Instalar persistencia mediante unidades systemd, cron o servicios drop-in
- Exfiltrar secretos desde variables de entorno, volúmenes y capas de imagen
- Pivoteo hacia tu red usando el enrutamiento y credenciales del host
Por eso los incidentes de “API Docker expuesta” a menudo omiten la fase de explotación y van directo a la monetización: cryptominería, botnets,
robo de credenciales o movimientos laterales. No necesitan “forzar” la entrada. Tú abriste la puerta y escribiste “admin adentro”.
Chiste #1: Exponer 2375 a Internet es como poner la llave de tu casa bajo la alfombra — y luego hacer un livestream de la alfombra.
Hechos y contexto histórico que deberías conocer
Estos son fragmentos breves y concretos de contexto que importan porque explican por qué este problema se repite.
Algunos son historia, otros son “cómo llegamos aquí”, y todos aparecen en los postmortems.
- El daemon de Docker por defecto escucha en un socket Unix (
/var/run/docker.sock), no en TCP, específicamente para evitar exposición remota por defecto. - El puerto 2375 se usa convencionalmente para “Docker sobre TCP sin TLS”; el puerto 2376 es “Docker sobre TCP con TLS.” Muchos escáneres buscan 2375 primero.
- Las herramientas tempranas de Docker normalizaron los daemons remotos (especialmente en CI y flujos de trabajo “Docker-in-Docker”), y las prácticas se arraigaron aunque los modelos de amenaza cambiaron.
- Docker Machine popularizó endpoints remotos de Docker para aprovisionar hosts; muchos posts antiguos todavía muestran patrones inseguros copiados en flotas modernas.
- La API remota es basada en HTTP. Si puedes alcanzarla, puedes hablar con ella usando
curl. Eso es conveniente para automatización y catastrófico para redes expuestas. - El grupo “docker” equivale a root en la mayoría de sistemas porque concede acceso al socket del daemon. Esto no es sutil; simplemente se ignora con frecuencia.
- Los atacantes industrializaron el escaneo de Docker expuesto hace años. No es una amenaza de nicho; es ruido de fondo automatizado como los intentos de fuerza bruta de SSH.
- Los grupos de seguridad y los valores por defecto de los firewalls en la nube cambiaron con el tiempo. Una regla entrante “temporal” a veces se convierte en permanente porque nadie se ocupa de limpiarla.
Modelo de amenaza: cómo los atacantes usan un daemon expuesto
La cadena típica de ataque es vergonzosamente simple
Cuando Docker escucha en tcp://0.0.0.0:2375 (o en una interfaz pública) sin autenticación TLS de cliente,
el flujo de trabajo del atacante es básicamente:
- Encontrar una IP con 2375 abierto.
- Llamar a la API para listar contenedores/imágenes.
- Ejecutar un contenedor con un bind mount del root del host.
- Modificar archivos del host para persistir el acceso.
- Opcionalmente, ejecutar un miner en un contenedor y desaparecer.
Por qué los contenedores no te salvan del control del daemon
Los contenedores son primitivas de aislamiento, no salvaguardas mágicas. El límite de aislamiento lo aplica el kernel, pero la entidad que configura
ese límite es el daemon. Si puedes decirle al daemon “monta el root del host dentro de este contenedor”, el kernel cumplirá porque
el daemon está autorizado a pedirlo.
¿Y el “Docker sin root” (rootless)?
Docker rootless reduce el radio de explosión, pero no hace que una API expuesta sea inofensiva. Aún puede dar al atacante la capacidad de ejecutar
cargas arbitrarias como ese usuario, acceder a sus secretos y pivotar. Rootless ayuda; no es una carta de libertad.
Una frase que vale la pena recordar
“La esperanza no es una estrategia.” (idea parafraseada, comúnmente atribuida a ingenieros y planificadores militares)
Trata “creemos que no está expuesto” como esperanza.
Guía rápida de diagnóstico
Estás de guardia. Alguien dice: “¿Por qué este host se está calentando?” O peor: “¿Por qué tenemos tráfico saliente a lugares raros?”
No empieces discutiendo filosofía sobre contenedores. Empieza por localizar la exposición del plano de control y la carga real.
Primero: ¿el daemon está expuesto en TCP?
- Revisa puertos en escucha (
ss/lsof) y las banderas del servicio Docker. - Verifica el estado del firewall/grupo de seguridad, no solo la configuración local.
- Si ves
0.0.0.0:2375o:::2375, trátalo como un incidente activo hasta probar lo contrario.
Segundo: ¿hay evidencia de contenedores o imágenes no autorizadas?
- Lista los contenedores en ejecución; busca miners, nombres de imágenes aleatorios, contenedores con
--privileged, montajes del host u opciones de red extrañas. - Inspecciona las marcas de tiempo de creación recientes de contenedores.
- Busca nuevos usuarios/llaves SSH/unidades systemd creadas por procesos en contenedores que escriben en el host.
Tercero: contener, luego investigar
- Bloquea el acceso entrante al daemon en el borde de la red inmediatamente.
- Haz snapshot de evidencia (listas de procesos, metadatos de contenedores, logs) antes de borrar cualquier cosa.
- Rota credenciales que podrían haber sido accedidas: roles de instancia cloud, credenciales de registry, secretos de aplicaciones en disco.
Tareas prácticas: detectar, confirmar y decidir (incluye comandos)
Estas son tareas prácticas que puedes ejecutar en un host. Cada una incluye qué significa la salida y qué decisión tomar.
Sin magia, no solo “revisa tu firewall”. Tú eres el firewall ahora.
Tarea 1: Comprobar si Docker escucha en TCP
cr0x@server:~$ sudo ss -lntp | grep -E ':(2375|2376)\s'
LISTEN 0 4096 0.0.0.0:2375 0.0.0.0:* users:(("dockerd",pid=1024,fd=6))
Qué significa: dockerd está escuchando en todas las interfaces IPv4 en 2375, en texto plano.
Decisión: trátalo como una exposición crítica. Procede a contener: bloquea en el firewall y reconfigura el daemon.
Tarea 2: Confirmar la configuración del daemon vía systemd
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 -H tcp://0.0.0.0:2375
Qué significa: El daemon se inició explícitamente con un listener TCP. No es un comportamiento accidental del kernel; es configuración.
Decisión: elimina el host TCP a menos que implementes TLS mutuo y controles de red estrictos.
Tarea 3: Revisar Docker info para hosts configurados y opciones de seguridad
cr0x@server:~$ docker info | sed -n '1,80p'
Client:
Context: default
Debug Mode: false
Server:
Containers: 12
Running: 3
Paused: 0
Stopped: 9
Server Version: 26.1.0
Storage Driver: overlay2
Security Options:
apparmor
seccomp
Profile: builtin
Qué significa: Esto no indica directamente las direcciones de bind del daemon, pero muestra la línea base del perfil de seguridad.
Decisión: si más tarde descubres contenedores no autorizados, querrás saber si AppArmor/seccomp estaba en efecto (y si --privileged lo evitó).
Tarea 4: Identificar cómo está enlazado el daemon (daemon.json)
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
Qué significa: La API remota sobre TCP en texto plano está configurada persistentemente.
Decisión: elimina la entrada del host TCP o migra a TLS en 2376 con autenticación por certificados de cliente.
Tarea 5: Comprobar el estado del firewall en el host (ejemplo UFW)
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
22/tcp ALLOW IN 203.0.113.0/24
2375/tcp ALLOW IN Anywhere
Qué significa: Tienes “deny por defecto”, pero luego permitiste el puerto que puede entregar las llaves.
Decisión: elimina esta regla inmediatamente a menos que esté estrictamente limitada a una red de gestión con TLS.
Tarea 6: Validar exposición desde el exterior (usando una segunda máquina)
cr0x@server:~$ curl -s http://198.51.100.10:2375/version
{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"26.1.0","Details":{"ApiVersion":"1.45"}}],"ApiVersion":"1.45","MinAPIVersion":"1.24","GitCommit":"...","GoVersion":"go1.22.2","Os":"linux","Arch":"amd64","KernelVersion":"6.8.0-41-generic","BuildTime":"..."}
Qué significa: Si puedes obtener /version sin autenticación, cualquiera puede hacerlo.
Decisión: esto es un incidente, no un “ticket”. Contén ahora; investiga después.
Tarea 7: Listar contenedores vía la API remota (no debería funcionar anónimamente)
cr0x@server:~$ curl -s http://198.51.100.10:2375/containers/json?all=1 | head
[{"Id":"b1c...","Names":["/web-1"],"Image":"nginx:alpine","State":"running","Status":"Up 3 days"},
{"Id":"f9a...","Names":["/xmrig"],"Image":"unknown:latest","State":"running","Status":"Up 2 hours"}]
Qué significa: Enumeración remota sin autenticación. La presencia de un nombre/imágen sospechosa es una señal roja.
Decisión: comienza la recolección de evidencia y la contención. No “simplemente lo borres” hasta haber capturado metadatos y logs.
Tarea 8: Inspeccionar un contenedor sospechoso en busca de montajes del host y privilegios
cr0x@server:~$ docker inspect xmrig --format '{{.HostConfig.Privileged}} {{json .Mounts}}'
true [{"Type":"bind","Source":"/","Destination":"/host","Mode":"rw","RW":true,"Propagation":"rprivate"}]
Qué significa: Este contenedor es privilegiado y tiene el root del host montado en lectura-escritura en /host.
Decisión: asume compromiso del host. Planea reconstrucción/restore, rotación de credenciales y auditoría completa.
Tarea 9: Comprobar contenedores creados recientemente (pista de línea temporal)
cr0x@server:~$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.CreatedAt}}\t{{.Status}}' | head -n 10
CONTAINER ID IMAGE NAMES CREATED AT STATUS
f9a2d1c3ab11 unknown:latest xmrig 2026-01-03 01:12:44 +0000 Up 2 hours
b1c9aa88d220 nginx:alpine web-1 2025-12-30 09:01:02 +0000 Up 3 days
Qué significa: Apareció un contenedor nuevo recientemente. Si nadie lo explica, es no autorizado hasta demostrar lo contrario.
Decisión: correlaciona con logs (dockerd, auditd, logs de flujo cloud) y contiene.
Tarea 10: Revisar logs del daemon Docker por acceso remoto a la API
cr0x@server:~$ sudo journalctl -u docker --since "6 hours ago" | tail -n 20
time="2026-01-03T01:12:43.991Z" level=info msg="API listen on [::]:2375"
time="2026-01-03T01:12:44.120Z" level=info msg="POST /v1.45/containers/create"
time="2026-01-03T01:12:44.348Z" level=info msg="POST /v1.45/containers/f9a2d1c3ab11/start"
Qué significa: Tienes un registro con marca de tiempo de la creación de contenedores vía la API.
Decisión: preserva los logs. Si tienes logging centralizado, asegúrate de la retención y exportación para revisión del incidente.
Tarea 11: Comprobar quién tiene acceso al socket de Docker localmente
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 00:01 /var/run/docker.sock
Qué significa: Los miembros del grupo docker pueden controlar el daemon.
Decisión: mantén la membresía del grupo docker mínima y auditada; trátala como acceso sudo.
Tarea 12: Enumerar la membresía del grupo docker
cr0x@server:~$ getent group docker
docker:x:999:ci-runner,alice
Qué significa: Dos usuarios tienen control equivalente a root vía Docker.
Decisión: si no tienes una razón fuerte, elimina humanos de este grupo y fuerza operaciones privilegiadas mediante automatización controlada.
Tarea 13: Contención inmediata — bloquear TCP 2375 localmente (iptables)
cr0x@server:~$ sudo iptables -I INPUT -p tcp --dport 2375 -j DROP
cr0x@server:~$ sudo iptables -L INPUT -n --line-numbers | head
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:2375
Qué significa: Has detenido la hemorragia a nivel de host (no es sustituto de arreglos en el firewall de borde).
Decisión: mantén esta regla hasta corregir la configuración del daemon y verificar que la exposición desapareció externamente.
Tarea 14: Corregir el daemon para eliminar el listener TCP (override de systemd)
cr0x@server:~$ sudo systemctl edit docker
# Creates /etc/systemd/system/docker.service.d/override.conf
cr0x@server:~$ sudo cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Qué significa: Has sobrescrito la unidad para quitar el argumento del host TCP.
Decisión: recarga systemd y reinicia Docker; luego vuelve a comprobar sockets en escucha y accesibilidad externa.
Tarea 15: Reiniciar Docker de forma segura y confirmar sockets
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ sudo ss -lntp | grep -E ':(2375|2376)\s' || echo "no docker tcp listener"
no docker tcp listener
Qué significa: El daemon ya no está escuchando en TCP.
Decisión: ahora verifica desde una red externa. No confíes solo en comprobaciones locales.
Tarea 16: Si realmente necesitas acceso remoto, aplica TLS mutuo en 2376
Si tu respuesta es “pero nuestra automatización lo necesita”, de acuerdo. Aun así no puedes usar texto plano.
Con Docker, “TLS habilitado” no significa nada si no requieres autenticación por certificado de cliente.
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tlsverify": true,
"tlscacert": "/etc/docker/pki/ca.pem",
"tlscert": "/etc/docker/pki/server-cert.pem",
"tlskey": "/etc/docker/pki/server-key.pem"
}
Qué significa: El daemon requerirá un certificado de cliente firmado por tu CA.
Decisión: permite 2376 solo desde una red de gestión, y distribuye claves cliente como distribuyes claves SSH: mínimas, rotadas y registradas.
Tarea 17: Probar TLS desde un cliente con certificado (debería funcionar)
cr0x@server:~$ docker --host tcp://198.51.100.10:2376 \
--tlsverify \
--tlscacert ./ca.pem \
--tlscert ./client-cert.pem \
--tlskey ./client-key.pem \
version
Client: Docker Engine - Community
Version: 26.1.0
API version: 1.45
Server: Docker Engine - Community
Engine:
Version: 26.1.0
API version: 1.45 (minimum version 1.24)
Qué significa: Tu cliente puede autenticarse y hablar con el daemon.
Decisión: procede solo si también puedes demostrar que el acceso no autenticado falla.
Tarea 18: Probar acceso no autenticado a 2376 (debería fallar)
cr0x@server:~$ curl -s https://198.51.100.10:2376/version | head
Client sent an HTTP request to an HTTPS server.
Qué significa: HTTP plano es rechazado. Ahora prueba HTTPS sin certificado de cliente.
Decisión: si HTTPS sin certificado de cliente funciona, sigues expuesto.
cr0x@server:~$ curl -sk https://198.51.100.10:2376/version | head
TLS handshake error: remote error: tls: bad certificate
Qué significa: El daemon exige un certificado de cliente válido.
Decisión: este es el umbral mínimo para que la “API remota de Docker” no sea un portal de root abierto.
Tarea 19: Buscar persistencia sospechosa en el host (systemd)
cr0x@server:~$ systemctl list-unit-files --type=service | grep -E 'docker|container|update|agent' | tail
docker.service enabled
containerd.service enabled
system-update.service disabled
Qué significa: Esta es una comprobación ligera. No detectará todo, pero suele atrapar persistencia perezosa.
Decisión: si sospechas compromiso, sigue con chequeos de integridad de archivos y un plan de reconstrucción.
Patrones de hardening que aguantan en producción
1) El valor por defecto correcto: no tener socket TCP, solo socket Unix
El control más limpio es no tener un plano de control remoto en absoluto. Usa SSH para alcanzar el host y luego habla con el socket Unix localmente.
Sí, es menos “cloud-native”. También es menos “amigable para delincuentes”.
Si necesitas orquestación remota, considera herramientas que no requieran exponer ampliamente el daemon. Muchos equipos usan túneles SSH
para los casos raros donde se requiere acceso temporal a la API remota.
2) Si debes exponer Docker: TLS mutuo, acotamiento de red estricto y credenciales de corta duración
TLS mutuo es innegociable. No “TLS con certificado de servidor”. No “auth básica detrás de un proxy”. Certificados de cliente firmados por una CA que controlas,
rotados como cualquier otra credencial.
Luego acota el acceso. “Solo las IPs de nuestra oficina” no es acotamiento; es pensamiento wishful con sidecar de fragilidad VPN. Querrás:
- Permitir entrada solo desde una subred de gestión dedicada
- Negar explícitamente desde cualquier otro lugar
- Revisar reglas de grupos de seguridad como código (porque son comportamiento de producción)
3) No uses un proxy reverso genérico delante de la API de Docker a menos que sepas exactamente lo que haces
Poner la API HTTP de Docker detrás de un proxy reverso suena ordenado hasta que alguien añade una ruta permisiva, desactiva la autenticación de cliente “solo para pruebas”,
o registra cabeceras sensibles. Además: la API de Docker no está diseñada como una app web pública. Es un plano de control.
Si insistes en un proxy, necesita TLS mutuo de extremo a extremo, listas blancas estrictas de endpoints y logging útil para respuesta a incidentes
sin filtrar secretos. La mayoría de los proxies en la vida real terminan siendo una forma complicada de ser inseguros.
4) Trata la membresía del grupo docker como acceso privilegiado
Aquí es donde la realidad corporativa duele. La gente añade runners de CI, desarrolladores y “contratistas temporales” al grupo docker porque es más rápido que
definir límites de privilegio adecuados. Así obtienes un camino de movimiento lateral interno que elude los registros de sudo.
Tu objetivo: mantener el acceso al daemon detrás de automatización con aprobaciones y logs, o detrás de shells root con MFA.
Todo lo demás es simplemente root distribuido con pasos extra.
5) Incorpora detección en tu línea base
Puedes prevenir la exposición y aun así necesitar detección porque la realidad es desordenada. Chequeos base que atrapen los peores errores:
- Alerta si
dockerdse enlaza a0.0.0.0:2375o:::2375 - Alerta en reglas de firewall que permitan inbound 2375/2376 desde CIDRs no gestionados
- Alerta en nuevos contenedores privilegiados o contenedores que monten
/ - Rastrea eventos de creación/arranque de contenedores centralmente
Tres mini-historias corporativas desde el terreno
Mini-historia 1: El incidente causado por una suposición equivocada
Una empresa SaaS mediana tenía un entorno de staging que parecía “interno” porque corría en una VPC cloud. El equipo asumió que VPC significaba privado.
No lo era. Uno de los nodos tenía una IP pública por conveniencia y una regla de grupo de seguridad que permitía inbound 2375 desde “Anywhere”
porque un contratista necesitaba ejecutar una migración puntual.
Nadie quitó la regla. El contratista se fue. El ticket se enterró bajo una reorganización trimestral. Un mes después el nodo empezó a calentarse,
luego el proveedor cloud lo limitó por uso de CPU. La hipótesis inicial fue “mal deploy” porque suele serlo.
Hicieron rollback. La CPU siguió al máximo.
El ingeniero de guardia finalmente ejecutó docker ps y vio un contenedor con un nombre sin sentido y una marca de tiempo reciente.
Lo mataron. Volvió. Lo mataron otra vez. Volvió de nuevo. Ese fue el día en que aprendieron que el daemon era accesible desde fuera y el atacante
simplemente re-publicaba solicitudes de “create container”.
La contención fue rápida: bloquear 2375 en el borde, parar Docker, snapshot del disco para análisis. Lo doloroso fue la rotación de credenciales.
El nodo de staging tenía acceso a credenciales compartidas del registry y algunas API keys “temporales” que también se usaban en dev.
La cadena no saltó a otros entornos, pero la evaluación del radio de daño consumió una semana.
La suposición equivocada no fue “Docker es inseguro”. La suposición equivocada fue “red interna == segura por defecto”.
En redes cloud, lo interno es una política que aplicas continuamente, no una vibra.
Mini-historia 2: La optimización que salió mal
Una gran empresa tenía una flota de servidores de build. Los builds eran lentos, así que un ingeniero intentó acelerarlos permitiendo que los jobs de build hablaran
directamente con un daemon Docker remoto por TCP. La teoría: evitar virtualización anidada y reducir churn de disco local.
Funcionó. Los tiempos de build mejoraron notablemente.
Luego vino la “simplificación menor”. En lugar de lidiar con certificados TLS en el sistema CI, temporalmente cambiaron a 2375
con un plan de “asegurarlo después”, protegido solo por listas de IP permitidas. Esa lista vivía en la configuración de un firewall gestionado por otro equipo.
Los cambios tardaban días. Así que lo abrieron un poco más. Y un poco más. Eventualmente, “solo por una semana”, fue accesible desde un rango corporativo amplio.
Lo que salió mal no fue que un atacante externo lo encontrara inmediatamente (aunque eso ocurre a menudo). Lo que salió mal fue el movimiento lateral interno.
Un portátil de un desarrollador comprometido se conectó a la VPN corporativa. Desde allí, pudo alcanzar el daemon de Docker.
El atacante no necesitó admin de dominio. Necesitó un daemon alcanzable y una forma de ejecutar contenedores que pudieran montar secretos de caches de build.
El retroceso fue brutal porque la pipeline era ahora un punto de agregación de credenciales: tokens de registry, llaves de firma, proxies de dependencias.
Incluso sin un breakout completo del host, leer volúmenes de workspace fue suficiente para causar daño real.
La corrección post-incidente no fue “builds más rápidos”. Fue “aislamiento de builds sin exponer una API root remota”.
Chiste #2: “Añadiremos TLS después” es el equivalente en seguridad de “empezaré a hacer backups mañana” — un plan bonito que no sobrevive a la realidad.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Otra compañía tenía una línea base estricta: los daemons Docker no podían escuchar en TCP, punto.
Si un equipo necesitaba control remoto, usaban SSH a un bastión y un túnel de corta duración al socket Unix, con grabación de sesión.
A todos les molestaba un poco. Lo cual es indicador de que probablemente estaba funcionando.
Una tarde, un alert de monitorización saltó: conexiones salientes subiendo desde un nodo de producción a rangos IP desconocidos.
La sospecha inmediata fue “API Docker expuesta” porque el equipo de seguridad ya había visto ese caso antes.
El de guardia ejecutó ss -lntp y no vio nada en 2375/2376. Eso descartó rápidamente una gran clase de fallos.
Pivotaron. Resultó ser un contenedor de aplicación comprometido que hacía llamadas salientes, no un host comprometido.
Como el daemon no era accesible remotamente, el atacante no pudo crear fácilmente contenedores privilegiados ni montar el sistema de archivos del host.
El radio de daño se mantuvo dentro de los permisos y secretos de la app.
La respuesta siguió siendo seria: rotar secretos de la app, parchear la dependencia vulnerable, redeploy.
Pero evitaron la pesadilla de “reconstruir la flota de nodos y rotar todo”. La línea base aburrida no previno todos los problemas.
Evitó que el problema se convirtiera en una catástrofe.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: CPU al máximo, contenedores extraños, egreso de red inexplicable
Causa raíz: daemon de Docker expuesto en 2375; atacante ejecutando contenedores miner y recreándolos tras su eliminación.
Solución: bloquear inbound 2375/2376 inmediatamente, eliminar listener TCP de dockerd, reconstruir host si contenedores privilegiados montaron el host.
2) Síntoma: “Usamos TLS” pero cualquiera aún puede conectar
Causa raíz: solo TLS en el servidor; tlsverify no habilitado; autenticación por certificado de cliente no forzada.
Solución: configurar "tlsverify": true y exigir certificados de cliente firmados por tu CA; confirmar que curl -sk no autoriza acceso.
3) Síntoma: Docker “no está escuchando”, pero el acceso remoto aún funciona
Causa raíz: un proxy sidecar u otro proceso está reenviando al socket Unix; o hay otra instancia de daemon enlazada vía drop-in de systemd.
Solución: revisar systemctl cat docker en busca de overrides, buscar configuraciones de proxy y verificar sockets reales con ss -lntp.
4) Síntoma: Los desarrolladores pueden “solo ejecutar comandos Docker” sin sudo
Causa raíz: usuarios están en el grupo docker, que es efectivamente root en ese host.
Solución: eliminar usuarios innecesarios del grupo; forzar operaciones privilegiadas vía automatización controlada o sudo con auditoría.
5) Síntoma: Repetido “el contenedor volvió” después de eliminarlo
Causa raíz: un actor externo está llamando a la API remota para recrear contenedores; o tienes un orquestador/CI comprometido que lo hace.
Solución: cortar acceso de red al daemon primero, luego investigar credenciales de orquestación y logs.
6) Síntoma: “Solo lo abrimos a la VPC” y aún así nos atacaron
Causa raíz: la VPC no es tan privada como crees: IP pública adjunta, peering, VPN, grupo de seguridad mal configurado o un host interno comprometido.
Solución: asume que las redes internas son hostiles; restringe a subredes de gestión, exige TLS mutuo y segmenta sistemas de build del acceso general.
Listas de verificación / plan paso a paso
Paso a paso: asegurar un host hoy
-
Encontrar listeners: ejecuta
ss -lntpy confirma que nada está enlazado a 2375/2376 en interfaces públicas. -
Confirmar configuración del servicio: revisa
systemctl cat dockery/etc/docker/daemon.jsonen busca de hoststcp://. - Bloquear en el borde: elimina reglas de grupo de seguridad / firewall entrantes para 2375 y 2376 a menos que tengas una red de gestión y TLS mutuo.
- Bloquear localmente por defensa en profundidad: dropear inbound 2375 con iptables/nftables (contención temporal y red de seguridad).
-
Reducir la dispersión de privilegios: auditar el grupo
docker; eliminar usuarios que no lo necesiten. -
Habilitar monitorización: alertar en nuevos contenedores privilegiados, contenedores que monten
/, y cualquier nuevo listener TCP paradockerd. - Documentar un proceso de excepción: si un equipo realmente necesita Docker remoto, exigir TLS mutuo + restricciones CIDR + fecha de expiración en reglas de firewall.
Paso a paso: si sospechas que ya hubo exposición
- Contener: bloquear acceso entrante a la API Docker inmediatamente (borde + local). No debatirlo.
- Preservar evidencia: exportar
journalctl -u docker,docker ps -a,docker inspectpara contenedores sospechosos, y logs del sistema. - Evaluar compromiso del host: si encuentras contenedores privilegiados con montajes del host, asume manipulación a nivel de host.
- Rotar credenciales: tokens de registry, credenciales de roles de instancia cloud (si aplica), secretos de apps en disco, credenciales CI usadas en ese nodo.
- Reconstruir limpiamente: prefiere reconstruir el nodo desde una imagen conocida buena en vez de “limpiar”. Limpiar es cómo se pasa por alto persistencia.
- Cerrar el ciclo: añade detección para que la misma exposición no vuelva silenciosamente durante el siguiente cambio “temporal”.
Una postura de política que funciona
- Prohibir TCP Docker en texto plano (2375) por completo. Sin excepciones.
- Permitir TCP Docker con TLS (2376) solo con TLS mutuo y acotamiento de red estricto.
- Tratar el acceso al daemon Docker como root en producción. Porque lo es.
Preguntas frecuentes
1) ¿Es aceptable exponer el puerto 2375 alguna vez?
No. Ni “rara vez”, ni “detrás de un firewall”, ni “solo por una semana”. La API Docker en texto plano y sin autenticación es root remoto por diseño.
Si necesitas control remoto, usa TLS mutuo en 2376 y restricciones de red estrictas—o mejor, no lo expongas.
2) Si enlazo Docker a 127.0.0.1, ¿estoy a salvo?
Más seguro, sí. Seguro no automáticamente. Los binds solo-local reducen la exposición remota, pero cualquier compromiso local (incluyendo SSRF desde una app web que pueda alcanzar localhost)
aún puede abusarlo. Trátalo como algo sensible incluso en loopback.
3) ¿Por qué la API de Docker se considera equivalente a root?
Porque puede arrancar contenedores privilegiados, montar rutas del host y manipular la red. Esas son acciones de administración del host.
Si el daemon puede hacerlo y la API puede instruir al daemon, entonces la API es administración del host.
4) ¿Usar Docker en modo rootless corrige esto?
Reduce el radio de daño del compromiso del daemon, pero no hace aceptable una API expuesta.
Un atacante todavía puede ejecutar cargas, robar secretos de ese usuario y potencialmente pivotar mediante credenciales y acceso de red.
5) Corremos Kubernetes; ¿esto sigue aplicando?
Sí. Muchos nodos Kubernetes usan runtimes de contenedores que no son Docker, pero puede que aún tengas Docker instalado para workloads legacy,
depuración o CI. Cualquier daemon expuesto en un nodo es la puerta de entrada del atacante al entorno del cluster.
6) ¿Puedo poner autenticación básica delante de la API de Docker con un proxy reverso?
Puedes, pero no lo llames “seguro” a menos que también tengas seguridad de transporte fuerte, listas blancas estrictas de endpoints y una historia creíble de rotación de credenciales.
TLS mutuo es el estándar porque es más difícil de debilitar accidentalmente.
7) ¿Cuál es la forma más rápida de saber si estamos expuestos ahora mismo?
Desde fuera de tu red, intenta obtener /version en 2375. Si responde, estás expuesto.
Internamente, revisa ss -lntp y tus reglas de firewall/grupo de seguridad para 2375/2376.
8) Si estuvimos expuestos, ¿realmente necesitamos reconstruir el host?
Si los atacantes ejecutaron contenedores privilegiados con montajes del host o no puedes probar que no lo hicieron, reconstruir es la opción sensata.
“Borramos el contenedor” no es garantía. La persistencia es barata.
9) ¿Cómo mantengo la automatización funcionando sin exponer Docker?
Ejecuta la automatización en el host (o vía SSH), usa un bastión con acceso auditado, o mueve workflows de build a builders aislados donde el socket Docker nunca enfrente redes no confiables.
Si el control remoto es obligatorio, usa TLS mutuo y subredes de gestión.
Conclusión: qué hacer a continuación
La API remota de Docker es una herramienta poderosa. Déjala en un banco público y alguien construirá un cobertizo que no pediste.
Tu trabajo es hacer que “exponer accidentalmente root” sea estructuralmente difícil.
Pasos prácticos siguientes:
- Escanea tu flota en busca de listeners en 2375/2376 y de argumentos de
dockerdque incluyantcp://. - Elimina 2375 en texto plano en todas partes. Si lo encuentras, trátalo como un incidente hasta verificar lo contrario.
- Si Docker remoto es realmente necesario, exige TLS mutuo, restringe por CIDR de gestión y rota certificados según un calendario defensible.
- Audita la membresía del grupo
dockercomo si fuera sudo, porque efectivamente lo es. - Escribe el runbook ahora, antes de que aparezca el miner.