Ubuntu 24.04: Docker + UFW = puertos abiertos sorpresa — Cierra la brecha sin romper contenedores

¿Te fue útil?

Cierras un host con UFW, dejas solo SSH abierto, despliegas algunos contenedores y te vas. Entonces un escáner (o peor, un cliente) encuentra tu servicio escuchando en Internet público de todos modos. Nada hace desconfiar más de un cortafuegos que uno que técnicamente hace lo que le dices—solo que no lo que tenías en mente.

En Ubuntu 24.04, Docker todavía puede abrir huecos alrededor de UFW de formas que sorprenden incluso a operadores competentes. Esto no es un discurso sobre “Docker es inseguro”. Se trata de entender el flujo de paquetes, la capa de interfaz iptables/nftables y los ganchos específicos que usa Docker—y luego colocar los controles correctos para que los contenedores sigan funcionando y los puertos dejen de aparecer en Internet.

Qué está pasando realmente: por qué UFW “funciona” y los puertos siguen abiertos

UFW no es un cortafuegos. UFW es un bibliotecario amable que guarda reglas de firewall en los cajones correctos. El portero real en la puerta es netfilter (iptables/nftables). Docker, mientras tanto, es el encargado VIP que se acerca al portero y añade unas notas de “dejen entrar a esta gente”—a menudo en un cajón que UFW no está mirando.

Cuando publicas un puerto con Docker (-p 0.0.0.0:8080:80 o un bloque ports: en Compose), Docker instala reglas NAT y de filtrado para que el tráfico que llega a la interfaz pública del host en ese puerto sea DNATeado a la IP del contenedor. Esas reglas se insertan en cadenas como PREROUTING (tabla nat) y FORWARD (tabla filter), y Docker también gestiona sus propias cadenas como DOCKER, DOCKER-ISOLATION-STAGE-* y, crucialmente, DOCKER-USER.

¿Dónde encaja UFW? UFW generalmente gestiona reglas en cadenas como ufw-before-input, ufw-user-input, ufw-before-forward, etc. Puede bloquear tráfico local al host hacia servicios enlazados al host, pero los puertos publicados en contenedores a menudo atraviesan la ruta FORWARD tras DNAT, y Docker ya los ha permitido. Así que UFW puede decir “deny 8080/tcp” todo el día y aun así ver paquetes reenviados al contenedor porque esa denegación se aplica en una cadena/orden diferente a la que asumiste.

Ubuntu 24.04 añade otra capa de confusión para el operador: las distribuciones modernas usan cada vez más nftables como motor subyacente, pero siguen exponiendo una interfaz compatible con iptables. Docker típicamente sigue programando reglas iptables (a través de iptables-nft en Ubuntu), que aparecen en el conjunto de reglas nft. UFW también escribe reglas, y la interacción es “quién se evalúa primero” más que “quién tiene la razón”. Los cortafuegos son deterministas; las suposiciones del operador no lo son.

Si recuerdas una regla: cuando Docker publica un puerto, trátalo como abrir un puerto en tu firewall, porque eso es efectivamente lo que hace—solo que no en el mismo lugar donde estabas mirando.

Broma corta #1: Los cortafuegos son como las puertas de oficina—cualquiera puede entrar si la persona con acceso admin sigue dejándolas abiertas “por conveniencia”.

Hechos interesantes y un poco de historia (para que el comportamiento tenga sentido)

  1. Docker eligió iptables temprano porque era universal. A mediados de la década de 2010, la red en Linux estaba fragmentada; iptables era el denominador común menos malo para NAT y reenvío.
  2. UFW es principalmente un generador de reglas iptables. Es una herramienta de políticas, no un filtro de paquetes en tiempo de ejecución. Escribe reglas; no “posee” netfilter.
  3. Los puertos publicados usan DNAT, no solo sockets en escucha. Por eso ss -lntp puede mostrar docker-proxy o un puerto enlazado, pero la verdadera magia es NAT + reenvío.
  4. Históricamente Docker usó un proxy en espacio de usuario para publicación de puertos. Las versiones más nuevas de Docker prefieren NAT en el kernel cuando es posible, pero el comportamiento difiere según versión y configuración; esto cambia lo que ves en ss.
  5. La cadena DOCKER-USER existe específicamente para que puedas anular Docker. Docker la añadió después de años en que los operadores pidieron un punto de gancho estable que Docker no reescribiera.
  6. El “deny” de UFW no se aplica automáticamente al tráfico reenviado. Los valores por defecto de UFW suelen estar orientados a INPUT (al host), no a FORWARD (a través del host hacia contenedores).
  7. El iptables de Ubuntu suele usar el backend nft. Muchos operadores todavía piensan en términos de iptables; bajo el capó, nft ejecuta las reglas (y las prioridades importan).
  8. Los grupos de seguridad en la nube pueden ocultar el problema. En VPCs estrictas, el firewall del host puede ser redundante; mueve el mismo host on‑prem y la sorpresa se convierte en titular.
  9. Docker sin root cambia el panorama. Con redes rootless, la exposición de puertos y la ruta de filtrado pueden diferir significativamente; no puedes aplicar ciegamente la misma receta de iptables.

Nada de esto es trivia oscura. Es la razón por la que el argumento “pero UFW está activado” se desmorona durante una revisión de incidentes.

Una idea para citar, parafraseada: Dr. Richard Cook (ingeniería de la resiliencia) tiene una idea conocida: las fallas ocurren cuando el trabajo normal y la complejidad se encuentran, no porque la gente sea descuidada.

Guía rápida de diagnóstico (comprueba primero/segundo/tercero)

Cuando alguien dice “UFW está activo pero el puerto está abierto”, no discutas. No adivines. Ejecuta una pequeña guía repetible y decide en base a la evidencia.

Primero: confirma qué está realmente expuesto

  • Desde otra máquina: escanea la IP pública del host por el/los puerto(s) reportado(s).
  • En el host: comprueba sockets en escucha y puertos publicados por Docker.
  • Decisión: ¿es un proceso del host, una publicación de contenedor o un balanceador/NAT aguas arriba?

Segundo: mapea la ruta de exposición

  • Encuentra el contenedor y el mapeo de puertos publicados.
  • Comprueba si el tráfico es INPUT (host) o FORWARD (hacia el contenedor vía DNAT).
  • Decisión: ¿necesitas bloquear en DOCKER-USER, ajustar la política de reenvío de UFW o cambiar los enlaces de publicación de Docker?

Tercero: inspecciona el orden de las reglas, no solo su presencia

  • Lista reglas de iptables/nft con números de línea y contadores.
  • Busca las reglas ACCEPT de Docker que aparecen antes de los drops de UFW en la cadena relevante.
  • Decisión: coloca la aplicación en DOCKER-USER (preferido) o reestructura explícitamente el manejo de forward de UFW.

Cuarto: aplica una corrección mínima, luego vuelve a probar desde fuera

  • Comienza con “deny por defecto para puertos publicados” en DOCKER-USER, luego permite solo lo que necesites.
  • Vuelve a ejecutar el escaneo externo y confirma que las comprobaciones de salud de los contenedores siguen pasando.
  • Decisión: si el tráfico de producción se rompe, revierte y cambia a “enlazar puertos publicados a IPs confiables” como intermedio más seguro.

Tareas prácticas: comandos, salidas y qué decisiones tomar

Estas son las tareas que realmente ejecuto durante un triage y endurecimiento. Cada una tiene: un comando, salida realista, lo que significa y la decisión que tomas.

Tarea 1: Confirma el estado de UFW y la política base

cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere

Qué significa: UFW está activado, el incoming por defecto es deny, pero routed (ruteado) está deshabilitado. El tráfico publicado por Docker comúnmente circula por FORWARD, no por INPUT.

Decisión: No asumas que “deny incoming” cubre contenedores. Debes comprobar el comportamiento de reenvío y las reglas de Docker.

Tarea 2: Comprueba qué puertos escuchan en el host

cr0x@server:~$ sudo ss -lntp
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=1186,fd=3))
LISTEN 0      4096   0.0.0.0:8080       0.0.0.0:*     users:(("docker-proxy",pid=4123,fd=4))
LISTEN 0      4096   127.0.0.1:9090     0.0.0.0:*     users:(("prometheus",pid=2201,fd=7))

Qué significa: El puerto 8080 está enlazado en todas las interfaces vía docker-proxy. Eso es una fuerte pista de que la exposición está relacionada con un contenedor, no con un demonio aleatorio del host.

Decisión: Identifica qué contenedor publicó el 8080 y si debería ser público.

Tarea 3: Lista claramente los puertos publicados por Docker

cr0x@server:~$ sudo docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
NAMES           IMAGE                 PORTS
webapp          ghcr.io/acme/web:1.7  0.0.0.0:8080->80/tcp
redis           redis:7               6379/tcp
metrics-gw      prom/pushgateway      0.0.0.0:9091->9091/tcp

Qué significa: webapp y metrics-gw están publicados públicamente. Redis no lo está (exposición solo dentro del contenedor, no al host).

Decisión: Si esos servicios deben ser privados, corrige los enlaces y/o aplica política en el firewall.

Tarea 4: Inspecciona la configuración de red de un contenedor

cr0x@server:~$ sudo docker inspect webapp --format '{{json .NetworkSettings.Ports}}'
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Qué significa: Publicado explícitamente en todas las interfaces. Esa es la “apertura de puerto sorpresa” en una línea JSON.

Decisión: O enlaza a una IP específica (por ejemplo 127.0.0.1) u obliga la política en DOCKER-USER para controlar la exposición.

Tarea 5: Identifica la interfaz pública del host y sus IPs

cr0x@server:~$ ip -br addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens3             UP             203.0.113.10/24 fe80::5054:ff:fe12:3456/64
docker0          DOWN           172.17.0.1/16

Qué significa: La IP pública está en ens3. El bridge de Docker es docker0. Conocer las interfaces importa para reglas dirigidas.

Decisión: Si solo quieres acceso local, enlaza puertos a 127.0.0.1 o a una interfaz privada, no a 0.0.0.0.

Tarea 6: Comprueba la política de reenvío de UFW y el forwarding del kernel

cr0x@server:~$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

Qué significa: El reenvío está habilitado (Docker normalmente lo habilita). El “routed disabled” de UFW no impide que el kernel reenvíe si las reglas lo permiten.

Decisión: Trata el filtrado FORWARD como obligatorio en hosts con contenedores.

Tarea 7: Mira el orden de la cadena FORWARD de iptables (donde vive la verdad)

cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT

Qué significa: Docker inserta DOCKER-USER al principio. Ese es tu punto de control. Si no lo usas, las propias reglas ACCEPT de Docker decidirán.

Decisión: Coloca políticas restrictivas en DOCKER-USER, no en reglas INPUT aleatorias de UFW.

Tarea 8: Ve la cadena DOCKER-USER (a menudo vacía por defecto)

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN

Qué significa: No hay restricciones; todo pasa a las reglas de Docker tras RETURN.

Decisión: Añade allow/deny explícitos aquí para controlar la exposición de puertos publicados.

Tarea 9: Inspecciona las reglas NAT que realizan el reenvío de puertos

cr0x@server:~$ sudo iptables -t nat -S DOCKER | sed -n '1,8p'
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9091 -j DNAT --to-destination 172.17.0.3:9091

Qué significa: El tráfico al puerto 8080 del host es DNATeado a la IP del contenedor. Por eso “cerrar el puerto” debe ocurrir también en la ruta de reenvío.

Decisión: No luches contra NAT con drops en INPUT. Controla el reenvío con DOCKER-USER (o cambia el enlace de publicación).

Tarea 10: Verifica desde el exterior (porque las comprobaciones locales mienten)

cr0x@server:~$ nc -vz 203.0.113.10 8080
Connection to 203.0.113.10 8080 port [tcp/http-alt] succeeded!

Qué significa: El puerto es alcanzable desde la red. Si ejecutas esto desde el mismo host, puedes obtener confianza falsa vía el enrutamiento loopback.

Decisión: Considera la validación externa como obligatoria, no opcional.

Tarea 11: Aplica una política “deny por defecto” para reenvío de contenedores en DOCKER-USER

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -o docker0 -j DROP
-A DOCKER-USER -j RETURN

Qué significa: El tráfico que llega desde la interfaz pública y se reenvía hacia docker0 es descartado antes de que se ejecuten las reglas allow de Docker.

Decisión: Si quieres “nada publicado es accesible desde Internet salvo que esté explícitamente permitido”, esta es la postura por defecto correcta.

Tarea 12: Añade una excepción de allow específica (solo lo que realmente quieres exponer)

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -p tcp --dport 80 -j ACCEPT
cr0x@server:~$ sudo iptables -L DOCKER-USER -n --line-numbers
Chain DOCKER-USER (1 references)
num  target   prot opt source      destination
1    ACCEPT   tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:80
2    DROP     all  --  0.0.0.0/0   0.0.0.0/0
3    RETURN   all  --  0.0.0.0/0   0.0.0.0/0

Qué significa: Permites tráfico reenviado hacia el puerto de destino 80 (puerto del contenedor tras DNAT) mientras descartas todo lo demás desde ens3 hacia docker0.

Decisión: Mantén esta lista de permisos con intención. Si no puedes explicar cada puerto permitido en una frase, no debería estar permitido.

Tarea 13: Haz que UFW coopere mejor con el reenvío (solo si insistes en una política centrada en UFW)

cr0x@server:~$ sudo grep -n '^DEFAULT_FORWARD_POLICY' /etc/default/ufw
19:DEFAULT_FORWARD_POLICY="DROP"

Qué significa: UFW está configurado para dropear el tráfico reenviado por defecto (bien), pero Docker puede tener reglas que aun así permitan reenvíos específicos.

Decisión: Manténlo en DROP. Si está en ACCEPT, cámbialo a DROP a menos que disfrutes de exposiciones sorpresa.

Tarea 14: Comprueba si UFW gestiona la cadena DOCKER-USER (normalmente no lo hace)

cr0x@server:~$ sudo iptables -S | grep -E 'ufw|DOCKER-USER' | sed -n '1,12p'
-N DOCKER-USER
-A FORWARD -j DOCKER-USER
-N ufw-before-forward
-N ufw-user-forward

Qué significa: UFW y Docker coexisten, pero UFW no está insertando política en DOCKER-USER por defecto. Por eso “ufw deny 8080” no ayudó.

Decisión: Decide quién es el propietario de la política de reenvío de contenedores. Mi voto: DOCKER-USER, gestionado vía control de configuración, no con ediciones manuales.

Tarea 15: Persiste los cambios de iptables tras el reinicio (porque los reinicios ocurren a las 3am)

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y iptables-persistent
Setting up iptables-persistent (1.0.20) ...
Saving current rules to /etc/iptables/rules.v4...
Saving current rules to /etc/iptables/rules.v6...

Qué significa: Tus reglas actuales de iptables se guardaron en disco y se restaurarán al arrancar.

Decisión: Si confías en reglas DOCKER-USER, persístelas. De lo contrario, la “corrección” desaparecerá tras mantenimiento.

Tarea 16: Vuelve a probar la exposición después de aplicar la política DOCKER-USER

cr0x@server:~$ nc -vz 203.0.113.10 8080
nc: connect to 203.0.113.10 port 8080 (tcp) failed: Connection timed out

Qué significa: El puerto ya no es accesible desde fuera (el timeout es típico de un drop). Este es el resultado que querías.

Decisión: Verifica que los servicios públicos requeridos sigan funcionando y documenta la política para que la próxima implementación no la “solucione” de vuelta.

Estrategias para cerrar el agujero sin romper contenedores

Tienes tres estrategias sensatas. Elige una deliberadamente. Mezclarlas de forma improvisada es como acabar con reglas que solo funcionan cuando la luna está en la fase correcta.

Estrategia A (recomendada): Usa DOCKER-USER como punto de aplicación

Docker promete no gestionar tus reglas DOCKER-USER. Ese es todo el sentido de la cadena. Si quieres “Docker puede hacer lo suyo, pero la política de seguridad sigue siendo mía”, DOCKER-USER es donde lo afirmas.

Modelo: drop por defecto para el tráfico desde la(s) interfaz(es) pública(s) hacia docker0; añade reglas allow para puertos de destino específicos o CIDR de origen; deja el tráfico interno east‑west intacto.

Pros: Estable, explícito, sobrevive al churn de contenedores, no depende de la idea de UFW sobre el reenvío. Funciona con Compose, patrones estilo Swarm y el networking bridge normal.

Contras: Otro lugar donde gestionar política. Debes persistir reglas y asegurar que la gestión de configuración las controle.

Estrategia B: Deja de publicar a 0.0.0.0 (enlaza a IPs específicas)

Si un servicio solo está pensado para acceso local o a través de un reverse proxy, no lo publiques ampliamente.

En Compose, en lugar de:

cr0x@server:~$ cat compose.yaml | sed -n '1,20p'
services:
  webapp:
    image: ghcr.io/acme/web:1.7
    ports:
      - "8080:80"

Usa enlaces explícitos:

cr0x@server:~$ cat compose.yaml | sed -n '1,20p'
services:
  webapp:
    image: ghcr.io/acme/web:1.7
    ports:
      - "127.0.0.1:8080:80"

Pros: Modelo mental simple. Ningún puerto es alcanzable externamente a menos que lo digas. Ideal cuando fronteas contenedores con Nginx/Traefik/Caddy en el host.

Contras: Fácil de revertir por descuido (“solo quita 127.0.0.1 para una prueba”), y no sirve si necesitas acceso externo pero solo desde redes específicas.

Estrategia C: Haz que UFW maneje el tráfico ruteado explícitamente (avanzado, frágil)

Puedes forzar más política dentro de las cadenas de reenvío de UFW y depender de las reglas route de UFW. Esto puede funcionar, pero estarás luchando contra el hecho de que Docker tiene su propia visión y actualiza reglas cuando los contenedores arrancan/paran.

Si eliges esta vía, debes:

  • Mantener la política forward de UFW en DROP
  • Usar reglas ufw route intencionalmente
  • Auditar la inserción de reglas de Docker después de cada actualización de Docker

Personalmente, prefiero DOCKER-USER porque está diseñado exactamente para esto. UFW es genial para servicios del host y políticas generales; no es ideal como única fuente de verdad para reenvío de contenedores a menos que disfrutes depurar los fines de semana.

Broma corta #2: NAT es el equivalente de red de una hoja de cálculo compartida—todos dependen de ella, nadie confía en ella y siempre está haciendo algo que no autorizaste.

Tres mini-historias corporativas desde el terreno

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

La compañía tenía una lista de verificación estándar de endurecimiento: habilitar UFW, permitir SSH desde la VPN, denegar todo lo demás. Se animó a los equipos a usar contenedores para herramientas internas, principalmente porque facilitaba las actualizaciones. Un ingeniero de plataforma incluyó UFW en la imagen base y lo llamó “guardarraíles”.

Un equipo de producto desplegó un nuevo dashboard interno en un contenedor y lo publicó en -p 8080:80 para poder comprobarlo rápidamente. Asumieron que “UFW lo bloquearía desde Internet”. No fue así. El servicio fue accesible desde cualquier parte, y en un día alguien fuera de la empresa empezó a hurgar.

La revisión post-mortem fue incómoda porque nadie hizo nada descabellado. El ingeniero no fue negligente; simplemente aplicó un modelo mental de firewall de host al reenvío de contenedores. El SRE de guardia lo reprodujo de inmediato: UFW negó 8080/tcp en INPUT, pero el tráfico no necesitó INPUT tras DNAT.

La solución fueron dos líneas en DOCKER-USER más una regla en sus convenciones de Compose: “no publicar puertos sin un binding IP explícito”. La corrección cultural fue mejor: actualizaron la lista de verificación para incluir un escaneo externo y una auditoría del orden de reglas cada vez que se instalaba Docker.

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

Otra organización tenía problemas de rendimiento en un host de borde muy ocupado. Alguien decidió “simplificar la red” deshabilitando agresivamente componentes que consideró redundantes. Redujeron el logging del firewall, eliminaron cadenas que no reconocían e intentaron hacer de UFW la única herramienta en juego.

Funcionó—hasta que una actualización de Docker reintrodujo sus cadenas y reordenó partes de la ruta FORWARD. De repente, un servicio que debía ser accesible solo desde una subred interna quedó accesible desde redes más amplias. El operador juró que nada había cambiado “en el firewall”, lo cual era técnicamente cierto: la configuración de UFW no cambió. Las reglas de Docker sí.

El verdadero problema no fue solo la exposición. Depurar se volvió más difícil porque su “optimización” eliminó las migas de pan: los contadores se reiniciaron, los registros estaban más silenciosos y no había un lugar establecido para poner política que Docker no sobrescribiera. Tuvieron que reaprender el flujo de reglas bajo presión.

Se recuperaron haciendo lo aburrido que intentaban evitar: definieron un documento único de política de firewall del host, lo aplicaron vía DOCKER-USER y mantuvieron UFW para servicios del host. El impacto en rendimiento fue insignificante comparado con el tiempo perdido en la respuesta al incidente.

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

Un equipo de servicios financieros ejecutaba hosts con contenedores siguiendo un estricto proceso de cambios. No era glamuroso. Cada host tenía un pequeño script de “invariantes de red” que se ejecutaba en CI y de nuevo en el host después del despliegue: listar puertos publicados, diff de cadenas iptables, ejecutar una prueba de conectividad externa desde una subred escáner controlada.

Una tarde de viernes, un desarrollador actualizó un Compose y accidentalmente cambió un mapeo de puerto de 127.0.0.1:9000:9000 a 9000:9000. En su portátil facilitó la vida. En producción habría expuesto una consola de administración.

La prueba de invariantes falló en CI porque su escáner pudo alcanzar el puerto 9000 en el host de staging. La pipeline detuvo el despliegue. Nadie tuvo que ser héroe, y nadie tuvo que pretender que “lo habría pillado en la revisión”. El script lo atrapó porque estaba diseñado para pillar exactamente esa clase de error.

Corrigieron el binding de Compose, rerunearon la prueba y desplegaron. No fue emocionante. Ese es el punto.

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

1) “UFW niega 8080 pero sigue siendo accesible”

Síntoma: ufw status no muestra regla de allow, y aun así escaneos externos conectan.

Causa raíz: El tráfico está siendo DNATeado y reenviado a un contenedor; las reglas INPUT de UFW no detienen el tráfico FORWARD que Docker ha permitido.

Solución: Aplicar política en DOCKER-USER (drop público→docker0) y luego permitir solo puertos necesarios; o enlazar puertos a 127.0.0.1.

2) “Puse un drop en DOCKER-USER y ahora todo se rompió”

Síntoma: Los contenedores no pueden acceder a Internet, o los servicios internos no se comunican.

Causa raíz: Regla DOCKER-USER demasiado amplia que descartó todo el reenvío, no solo el ingress público a docker0. Error común: dropear sin limitar por interfaz.

Solución: Restricción de reglas: -i ens3 -o docker0 para ingreso a contenedores, y dejar -i docker0 -o ens3 (egress) intacto a menos que realmente lo quieras controlar.

3) “Después del reinicio, los puertos están abiertos otra vez”

Síntoma: Lo arreglaste ayer; hoy volvió.

Causa raíz: Las reglas DOCKER-USER se añadieron de forma interactiva y no se persistieron; Docker luego recreó sus propias reglas al arrancar.

Solución: Persiste reglas con iptables-persistent o gestiona las reglas vía una unidad systemd/gestión de configuración que se ejecute después de que Docker arranque.

4) “Solo algunos puertos publicados están bloqueados; otros se cuelan”

Síntoma: El puerto 8080 está bloqueado, pero 9091 sigue accesible.

Causa raíz: Tu allowlist/denylist se basa en puertos del host, pero tu coincidencia en DOCKER-USER es sobre el puerto de destino post-DNAT (puerto del contenedor). O al revés.

Solución: Decide sobre qué vas a coincidir. En DOCKER-USER, coincidir con --dport suele referirse al puerto del contenedor tras DNAT en la ruta FORWARD. Valida con contadores y prueba cada puerto.

5) “La regla ufw route no hizo nada”

Síntoma: Añades una regla route en UFW, pero la conectividad no cambia.

Causa raíz: Orden de reglas: las reglas accept de FORWARD de Docker pueden seguir permitiendo tráfico antes de las cadenas route de UFW, dependiendo de cómo se insertan las reglas y qué cadenas se alcanzan.

Solución: Prefiere DOCKER-USER para control de ingreso a contenedores. Si debes usar routing de UFW, confirma el orden de cadenas con iptables -S FORWARD y revisa contadores.

6) “Está cerrado desde fuera, pero la monitorización interna dice que está abierto”

Síntoma: Comprobaciones internas de salud tienen éxito; las externas fallan; alguien lo llama falso positivo.

Causa raíz: Ruta diferente: las comprobaciones internas pueden venir desde una interfaz privada, una VPN o loopback, no desde la interfaz pública que estás filtrando.

Solución: Valida desde la misma perspectiva de red que tu modelo de amenaza (Internet/Límite VPC). Escribe reglas que permitan explícitamente fuentes internas/VPN y descarten fuentes públicas.

Listas de verificación / plan paso a paso

Plan de endurecimiento paso a paso (haz esto en cada host con Docker)

  1. Inventario de exposición: lista puertos publicados y quién los posee (docker ps, ss).
  2. Decide la política: qué servicios son públicos, cuáles son privados y cuáles deben ser solo por VPN.
  3. Establece una postura por defecto: drop por defecto de interfaz pública → tráfico del bridge de Docker en DOCKER-USER.
  4. Añade allows explícitos: solo para servicios pensados para ser públicos (o desde CIDR específicos).
  5. Enlaza servicios privados: cambia Compose/Docker run para publicar en 127.0.0.1 o una IP privada.
  6. Persiste reglas: asegúrate de que las reglas DOCKER-USER sobrevivan reinicios y reinicios de Docker.
  7. Re-prueba externamente: realiza una comprobación de puertos desde fuera de la red del host.
  8. Escribe invariantes: añade cheques en CI que fallen si un Compose publica a 0.0.0.0 sin aprobación.
  9. Operationaliza auditorías: escaneo periódico + diff de reglas de firewall y puertos publicados.

Receta mínima “segura por defecto” para DOCKER-USER

Este es el baseline que me gusta para hosts expuestos a Internet con networking bridge de Docker. Ajusta nombres de interfaz y puertos.

cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i ens3 -o docker0 -p tcp --dport 443 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 3 -i ens3 -o docker0 -p tcp --dport 80 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 4 -i ens3 -o docker0 -j DROP
cr0x@server:~$ sudo iptables -L DOCKER-USER -n --line-numbers
Chain DOCKER-USER (1 references)
num  target     prot opt source      destination
1    ACCEPT     all  --  0.0.0.0/0   0.0.0.0/0   ctstate RELATED,ESTABLISHED
2    ACCEPT     tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:443
3    ACCEPT     tcp  --  0.0.0.0/0   0.0.0.0/0   tcp dpt:80
4    DROP       all  --  0.0.0.0/0   0.0.0.0/0
5    RETURN     all  --  0.0.0.0/0   0.0.0.0/0

Qué hace: Permite flujos establecidos y solo reenvía nuevo ingreso público a contenedores en 80/443. Todo lo demás desde la interfaz pública hacia docker0 se descarta.

Qué no hace: No asegura tus contenedores internamente, no reemplaza TLS y no arregla la autenticación de la aplicación. Solo evita que exposiciones accidentales se conviertan en política.

Preguntas frecuentes

1) ¿Por qué Docker “omite” UFW?

No tanto omite como usa una ruta de paquetes diferente. Los puertos publicados de contenedores típicamente usan NAT y la cadena FORWARD; las reglas de UFW que estableces a menudo apuntan a INPUT. Diferentes cadenas, diferentes resultados.

2) ¿Esto es específico de Ubuntu 24.04?

No, pero el backend nftables común en Ubuntu 24.04 y las configuraciones modernas hacen que la interacción sea más fácil de malinterpretar. El comportamiento central existe donde Docker gestiona reglas iptables.

3) ¿Debería deshabilitar la gestión de iptables de Docker?

Normalmente no. Deshabilitar la integración iptables de Docker puede romper la red y la publicación de puertos a menos que reemplaces completamente sus reglas por tu cuenta. Si te lo estás planteando, probablemente no quieras esa carga de mantenimiento.

4) ¿Cuál es la solución rápida más segura durante un incidente?

Añade un DROP acotado en DOCKER-USER para interfaz pública → docker0, luego añade ACCEPTs explícitos para los puertos que deban seguir públicos. Vuelve a probar desde fuera inmediatamente.

5) Si enlazo 127.0.0.1:8080:80, ¿es suficiente?

Es un control fuerte para “acceso solo host”, especialmente cuando un reverse proxy termina el tráfico externo. Pero no ayuda si necesitas que el servicio sea accesible desde una subred privada/VPN sin proxy—en ese caso querrás reglas DOCKER-USER que permitan por CIDR de origen.

6) ¿Afecta también IPv6?

Sí, y a menudo se olvida. Si IPv6 está habilitado y Docker publica en IPv6, necesitas política equivalente en ip6tables/nft para v6. No asumas que reglas v4 cubren la exposición v6.

7) ¿Por qué no confiar solo en los grupos de seguridad de la nube?

Los security groups son estupendos, pero no siempre están disponibles (on‑prem), y no te protegen de movimientos laterales internos de la misma manera. Además: los operadores suelen copiar cargas entre entornos. La política del host debe ser correcta por sí misma.

8) ¿Cómo evito regresiones cuando los equipos cambian archivos Compose?

Impón convenciones: no usar "8080:80" desnudo para servicios internos; exigir binding IP explícito o una etiqueta de revisión. Añade CI que analice Compose y falle cuando un puerto se publique en 0.0.0.0 inesperadamente.

9) ¿Las reglas DOCKER-USER romperán el tráfico entre contenedores?

No si las scopeas correctamente. Céntrate en el ingreso desde la interfaz pública hacia docker0. Deja intacto el tráfico cuyo origen es docker0 a menos que tengas un requisito específico de control de salida.

Conclusión: pasos prácticos siguientes

Si ejecutas Docker en Ubuntu 24.04 y asumes que solo UFW controla la exposición, te has tendido una trampa para tu yo futuro. La solución no es dramática: entiende que los puertos publicados de contenedores viven en la ruta de forwarding/NAT y luego aplica la política donde Docker te da un gancho estable—DOCKER-USER.

Pasos siguientes que no te harán perder el tiempo:

  1. Ejecuta las tareas de inventario: ss, docker ps y una prueba de conectividad externa.
  2. Añade un drop por defecto de interfaz pública → docker0 en DOCKER-USER y luego permite solo lo que deba ser público.
  3. Cambia servicios internos para enlazar puertos publicados a 127.0.0.1 (o a una IP privada) para que los accidentes no se conviertan en exposiciones.
  4. Persiste tus reglas y añade una prueba de regresión en CI. Aburrido. Correcto. Efectivo.

No buscas “ganarle” a Docker. Busca que tu intención sea inequívoca para el filtro de paquetes. Ese es todo el juego.

← Anterior
Perfil mínimo de firewall en Debian 13: qué permitir y qué bloquear (sin paranoia)
Siguiente →
MariaDB vs Percona Server: cuando los casos límite dañan la replicación

Deja un comentario