Activaste UFW. Pusiste “deny incoming”. Incluso sentiste una pequeña oleada de rectitud.
Luego, un escaneo rápido muestra que el puerto de tu contenedor sigue accesible desde Internet. Genial.
Esto no es que UFW sea “malo” ni que Docker sea “inseguro por defecto” en un sentido caricaturesco.
Es una interacción predecible entre la automatización de iptables de Docker y la forma en que UFW organiza sus reglas.
Si gestionas sistemas en producción, necesitas entender la ruta del paquete y aplicar la política en el único punto donde Docker no puede “salirse con la suya”.
El modelo mental: a dónde van realmente tus paquetes
Cuando publicas un puerto con Docker (-p 8080:80), Docker no le pide educadamente permiso a UFW.
Programa el filtrado de paquetes del kernel (iptables/nftables) para hacer DNAT y aceptar tráfico, porque “hacer que funcione” vence a “esperar a los humanos” en el diseño por defecto.
UFW, mientras tanto, es un gestor de reglas. Escribe un conjunto curado de cadenas y saltos en iptables (o nftables en algunos sistemas),
y lo hace en un orden específico.
El orden lo es todo. La primera regla que coincida gana.
Aquí está el problema central: Docker inserta reglas en las tablas nat y filter que pueden aceptar tráfico reenviado hacia contenedores
antes de que la postura de “deny incoming” de UFW tenga siquiera oportunidad de opinar.
El tráfico no es “entrante al host” en el sentido que piensas; se reenvía a través del host hacia un contenedor.
Ruta del paquete, simplificada pero precisa
Cuando un paquete llega por tu interfaz pública destinado a un puerto publicado:
- PREROUTING (nat): Docker hace DNAT del destino a la IP/puerto del contenedor.
- FORWARD (filter): El kernel reenvía el paquete desde la interfaz del host hacia el bridge de Docker.
- DOCKER / DOCKER-USER (filter): Las cadenas de Docker deciden qué pasa.
- Contenedor: El servicio recibe el paquete.
Las reglas habituales de “deny incoming” de UFW viven mayormente alrededor de la cadena INPUT.
Pero los paquetes reenviados golpean FORWARD, no INPUT.
Así que puedes cerrar la puerta principal mientras dejas la puerta lateral bien abierta.
La solución no es mística. Aplicarás tu política en la ruta que Docker usa: la cadena DOCKER-USER.
Esa cadena existe específicamente para que puedas aplicar tus propias reglas antes de la lógica de aceptación de Docker.
Por qué UFW “pierde” frente a Docker (y por qué eso no es un bug)
UFW tiene una opinión: asume que el host es el endpoint.
Docker tiene otra opinión: asume que el host es un router para las redes de contenedores.
Ponlos juntos y obtienes una disputa de custodia en la red.
La publicación de puertos de Docker se implementa con reglas de iptables que:
- DNAT del tráfico en
nat/PREROUTINGynat/OUTPUTpara conexiones locales. - Aceptan el reenvío en
filter/FORWARDhaciadocker0(o un bridge definido por el usuario). - Mantienen sus propias cadenas (como
DOCKER) e insertan saltos lo suficientemente temprano como para importar.
UFW puede controlar el reenvío, pero muchas instalaciones dejan el reenvío permisivo o no conectan las reglas de forward de UFW para preceder a Docker.
Y si tu modelo mental es “deny incoming significa que nada llega a mi máquina”, pasarás por alto la distinción entre INPUT y FORWARD.
Una frase que se ha quedado con la gente de ops por décadas—llámala una idea parafraseada de Gene Kranz (Flight Director de la NASA):
idea parafraseada: “Firme y competente” supera a lo ingenioso cuando las cosas van mal.
Los cortafuegos no son lugar para la astucia. Ahí quieres aburrido y correcto.
Broma #1: Una regla de firewall “temporal” añadida durante un incidente tiene la misma vida media que la basura radiactiva—alguien más la heredará.
Datos interesantes e historia corta para usar a las 3 a.m.
- iptables evalúa en orden: las reglas se comprueban de arriba hacia abajo; la primera coincidencia gana. “Añadí un deny” no significa nada si está debajo de un accept.
- Docker popularizó host-como-router: las primeras versiones de Docker usaban por defecto networking por bridge y NAT, convirtiendo efectivamente cada host en un pequeño router de borde.
- UFW es un front-end: no “corre junto a” iptables; escribe reglas de iptables y gestiona cadenas. Si algo más edita iptables, UFW no es psíquico.
- La cadena DOCKER-USER existe por una razón: se añadió para que los administradores pudieran hacer cumplir políticas antes de las reglas gestionadas por Docker sin pelear constantemente con Docker.
- FORWARD suele pasarse por alto: muchos equipos endurecen INPUT pero olvidan que la política de reenvío importa una vez que entran contenedores y bridges.
- conntrack es el pegamento stateful: los aceptes “ESTABLISHED,RELATED” pueden hacer que un puerto parezca “abierto” para flujos existentes incluso después de “cerrarlo”.
- La publicación se enlaza a 0.0.0.0 por defecto: a menos que especifiques una IP, Docker expondrá en todas las interfaces del host. Eso incluye las públicas.
- La migración a nftables es desigual: las distribuciones modernas pueden usar nftables por defecto, pero las interacciones entre Docker y UFW aún pueden mediarse a través de capas de compatibilidad de iptables.
Guía de diagnóstico rápido
Cuando alguien dice “UFW está activado, pero el puerto sigue abierto”, no debatas filosofía. Ejecuta la guía.
El objetivo es encontrar dónde ocurre el accept: INPUT, FORWARD o DNAT de Docker.
Primero: confirma qué está realmente expuesto
- Revisa los puertos publicados por Docker y sus direcciones de bind.
- Confirma los sockets que escuchan en el host.
- Prueba desde un punto de vista externo (no desde el mismo host).
Segundo: mapea la ruta del paquete
- Inspecciona reglas de iptables/nftables: especialmente PREROUTING en
naty FORWARD enfilter. - Encuentra las cadenas de Docker y su orden de salto.
- Revisa la cadena
DOCKER-USER: ¿existe y hace algo?
Tercero: decide la estrategia de contención correcta
- Si el servicio debe ser sólo interno: enlaza los puertos publicados a
127.0.0.1o a una interfaz privada. - Si debe ser público pero restringido: haz cumplir la política en
DOCKER-USERpor IP de origen, interfaz o puerto de destino. - Si necesitas política perimetral real: prefiere un firewall dedicado al frente (SG/NACL de la nube, appliance hardware, o un firewall host con política estricta en
DOCKER-USER).
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas se pueden ejecutar en un host Ubuntu típico con Docker y UFW. Ajusta los nombres de interfaz según sea necesario.
Cada tarea incluye: comando, qué significa la salida y qué decisión tomar.
Task 1: Confirmar estado de UFW y política por defecto
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 203.0.113.0/24
Significado: “disabled (routed)” es la señal de alarma: el tráfico reenviado no está siendo gobernado por UFW.
Decisión: Si dependes de UFW para bloquear la exposición de Docker, debes abordar el tráfico routado/reenvío (o aplicar la política en DOCKER-USER).
Task 2: Listar los puertos publicados por Docker con direcciones de bind
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
web nginx:alpine 0.0.0.0:8080->80/tcp
metrics prom/prometheus 127.0.0.1:9090->9090/tcp
Significado: web está expuesto en todas las interfaces; metrics es sólo localhost y no será accesible externamente.
Decisión: Si un servicio no debería ser público, rebindearlo a 127.0.0.1 es la ganancia más simple.
Task 3: Verificar qué está escuchando en el host (sockets)
cr0x@server:~$ sudo ss -lntp | awk 'NR==1 || /:8080|:9090|:22/'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=1542,fd=4))
LISTEN 0 4096 127.0.0.1:9090 0.0.0.0:* users:(("docker-proxy",pid=1611,fd=4))
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=912,fd=3))
Significado: Docker (o docker-proxy) está escuchando en 0.0.0.0:8080, que está genuinamente expuesto a menos que el filtrado de paquetes lo bloquee.
Decisión: No asumas “está en un contenedor, así que está aislado”. Trátalo como cualquier otro listener.
Task 4: Confirmar interfaces públicas y direcciones del host
cr0x@server:~$ ip -br addr
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 198.51.100.10/24 fe80::5054:ff:fe12:3456/64
docker0 DOWN 172.17.0.1/16
Significado: Cualquier cosa enlazada a 0.0.0.0 es alcanzable vía ens3 a menos que se filtre.
Decisión: Decide si la exposición en esa interfaz es intencional; si no, enlaza explícitamente o bloquea en esa interfaz.
Task 5: Inspeccionar las reglas NAT de Docker (donde ocurre DNAT)
cr0x@server:~$ sudo iptables -t nat -S | sed -n '1,120p'
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-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 9090 -j DNAT --to-destination 172.17.0.3:9090
Significado: El tráfico del puerto 8080 se está DNATeando a un contenedor. Las reglas de INPUT de UFW no detienen el DNAT.
Decisión: Debes controlar la aceptación del reenvío (filter/FORWARD) o aplicar la política en DOCKER-USER.
Task 6: Inspeccionar la cadena FORWARD de la tabla filter (el típico “gotcha”)
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
Significado: Incluso con la política FORWARD en DROP, Docker instala aceptes. Crucialmente, salta a DOCKER-USER primero.
Decisión: Pon tus reglas de deny/allow en DOCKER-USER para preceder los accepts de Docker.
Task 7: Comprobar qué hay ahora en DOCKER-USER
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN
Significado: Sin política. Todo llega a RETURN y luego aplican las reglas permisivas de reenvío de Docker.
Decisión: Añade política explícita aquí. DOCKER-USER vacío es “confíame en la red” de manual.
Task 8: Añadir un deny por defecto para tráfico reenviado hacia contenedores, y luego permitir lo que necesites
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -j DROP
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -o docker0 -p tcp --dport 8080 -s 203.0.113.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -o docker0 -p tcp -m tcp --dport 8080 -s 203.0.113.0/24 -j ACCEPT
-A DOCKER-USER -i ens3 -o docker0 -j DROP
-A DOCKER-USER -j RETURN
Significado: Se descarta el tráfico desde la interfaz pública hacia docker0 salvo TCP/8080 desde una subred confiable.
Decisión: Esta es la postura de “hacer la exposición explícita”. Añade allows por servicio/puerto y conserva el drop.
Task 9: Confirmar que los contadores se mueven donde esperas
cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
12 720 ACCEPT tcp -- ens3 docker0 203.0.113.0/24 0.0.0.0/0 tcp dpt:8080
305 18300 DROP all -- ens3 docker0 0.0.0.0/0 0.0.0.0/0
0 0 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Significado: Estás descartando intentos de acceso y permitiendo las fuentes previstas.
Decisión: Si los contadores en DROP suben inesperadamente, probablemente expusiste un puerto que no sabías que estaba en uso.
Task 10: Hacer las reglas persistentes (o desaparecerán al reiniciar)
cr0x@server:~$ sudo apt-get update
cr0x@server:~$ sudo apt-get install -y iptables-persistent
cr0x@server:~$ sudo netfilter-persistent save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables save
Significado: Tus reglas actuales de iptables se guardan y se restaurarán al arrancar.
Decisión: Si gestionas infraestructura con gestión de configuración, codifica estas reglas allí en lugar de depender del estado del servidor “pet”.
Task 11: Enlazar un puerto publicado a localhost (a menudo la mejor solución)
cr0x@server:~$ docker run -d --name internal-admin -p 127.0.0.1:8081:80 nginx:alpine
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}' | grep internal-admin
internal-admin 127.0.0.1:8081->80/tcp
Significado: El servicio solo es accesible desde el propio host (o vía túnel SSH/reverse proxy).
Decisión: Usa esto para dashboards, paneles de administración y cualquier cosa a la que accedas vía bastión de todas formas.
Task 12: Verificar la política routada de UFW si insistes en que UFW gestione el forwarding
cr0x@server:~$ sudo ufw status verbose | grep -i routed
Default: deny (incoming), allow (outgoing), disabled (routed)
Significado: El tráfico routado está deshabilitado; UFW no está gobernando el forward.
Decisión: O habilitas la política routada e integras cuidadosamente, o dejas de fingir que UFW por sí sola controla la exposición de Docker y usas DOCKER-USER.
Task 13: Comprobar la configuración de forwarding de UFW en sysctl (el kernel puede vetarte)
cr0x@server:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
Significado: El reenvío IP está habilitado (común en hosts con Docker).
Decisión: Si deshabilitas el forwarding para “arreglar” la exposición, espera que la red de contenedores se rompa. Eso es un instrumento contundente, no un plan.
Task 14: Ver el orden completo de reglas alrededor de Docker y UFW
cr0x@server:~$ sudo iptables -S | sed -n '1,120p'
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-N ufw-before-input
-N ufw-user-input
-A INPUT -j ufw-before-input
-A INPUT -j ufw-user-input
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER
-A DOCKER-USER -j RETURN
Significado: Docker engancha FORWARD temprano. UFW engancha principalmente INPUT. Esa es la historia completa en 10 líneas.
Decisión: Deja de esperar que la política de INPUT gobierne el tráfico reenviado.
Task 15: Comprobación externa rápida desde otra máquina (prueba de realidad)
cr0x@server:~$ nc -vz 198.51.100.10 8080
Connection to 198.51.100.10 8080 port [tcp/http-alt] succeeded!
Significado: El puerto es accesible externamente.
Decisión: Si eso no estaba previsto, arregla la dirección de bind o las reglas en DOCKER-USER, y vuelve a probar hasta que falle desde fuentes no confiables.
Task 16: Confirmar la opción “iptables management” de Docker (y por qué normalmente no debes desactivarla)
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "journald",
"iptables": true
}
Significado: Docker está gestionando iptables, que es el comportamiento predeterminado normal.
Decisión: No pongas esto en false a menos que estés listo para hacerte completamente responsable del NAT/reenvío/aislamiento y depurar roturas extrañas después.
Patrones de bloqueo que realmente funcionan
Patrón A: Enlazar a localhost o a una interfaz privada siempre que sea posible
Si un servicio se usa solo por procesos en el host, un reverse proxy o un túnel SSH, enlázalo a 127.0.0.1.
Esto es más limpio que las reglas de firewall porque el kernel nunca expone el socket públicamente.
En Docker Compose, eso significa:
publicar como 127.0.0.1:PORT:PORT.
Es aburrido. Es efectivo. Aún puedes poner Nginx/Traefik/Caddy delante para manejar el tráfico público deliberadamente.
Patrón B: Usar DOCKER-USER como puerta de política para puertos publicados
Piensa en DOCKER-USER como tu “cadena del equipo de seguridad”.
Pon denies por defecto allí para el tráfico que entra desde interfaces públicas hacia los bridges de Docker,
y luego añade allows explícitos para lo que debería ser accesible.
El valor predeterminado seguro en un host expuesto a Internet es:
- Permitir established/related (o dejar que Docker lo maneje).
- Permitir puertos publicados explícitamente desde orígenes explícitos.
- Descartar el resto desde la(s) interfaz(es) pública(s) hacia el/los bridge(s) de Docker.
Patrón C: Poner un borde real delante de los hosts Docker
Los firewalls en host son buenos, pero no sustituyen las salvaguardas a nivel de red.
Si puedes usar un security group de la nube, un appliance firewall dedicado, o incluso un nodo de ingreso separado, hazlo.
La defensa en profundidad no es un eslogan; es lo que evita que “un mal Compose” arruine tu semana.
Patrón D: Preferir un reverse proxy de ingreso en vez de publicar cada servicio
Si cada contenedor publica su propio puerto al mundo, has construido un zoológico de puertos.
Un reverse proxy centraliza TLS, autenticación y decisiones de exposición. También hace que el resultado de los escaneos sea menos emocionante.
Broma #2: Si publicas -p 0.0.0.0:2375:2375 para la API de Docker, felicidades—has inventado root remoto.
Tres micro-historias corporativas desde el frente
1) El incidente causado por una suposición equivocada
Una compañía SaaS mediana migró una aplicación heredada a contenedores en un puñado de VPS.
El plan de migración era razonable: mantener la huella pequeña, los costos bajos y confiar en UFW porque “ya lo usamos en todas partes”.
La revisión de seguridad fue un ejercicio de lista de verificación. UFW: activado. Default deny: activado. Enviar.
Unas semanas después, se desplegó una nueva herramienta interna como contenedor con -p 8080:8080.
Estaba pensada para uso administrativo interno, accedida vía VPN. El ingeniero asumió que UFW bloquearía el tráfico no VPN por “deny incoming”.
No lo hizo. El puerto era accesible desde la IP pública. No de forma ruidosa ni dramática—simplemente accesible.
La primera señal no fue una alerta; fue una factura sorpresa por tráfico saliente y la queja de que la herramienta “iba lenta”.
Alguien había encontrado el endpoint y estaba forzando credenciales. La autenticación de la herramienta era decente, pero no diseñada para Internet abierto.
Los logs mostraron muchos intentos fallidos y algunos exitosos desde IPs comunes.
El postmortem no trató de culpar a Docker o UFW.
Habló de la brecha del modelo mental: el equipo trató a los contenedores como procesos “dentro del host” en vez de endpoints alcanzados vía reenvío y DNAT.
Cuando aplicaron un drop por defecto en DOCKER-USER y rebindeó puertos internos a localhost, la categoría de fallo desapareció.
La conclusión incómoda: “Firewall activado” no es un control de seguridad. Una política probada sí lo es.
2) La optimización que salió mal
Un equipo empresarial ejecutaba docenas de servicios en contenedores por nodo y quería “red más rápida”.
Alguien decidió que la programación de iptables de Docker era “sobrecarga” y desactivó la integración de iptables en la configuración del daemon.
La idea era gestionar todo el firewall con UFW y mantener el sistema “limpio”.
Los primeros días parecieron bien, porque la mayor parte del tráfico este-oeste estaba en el mismo host y el estado de conntrack enmascaró algunos problemas.
Luego ocurrió un reinicio rutinario del host—ventana de parches, sin drama.
De repente, un subconjunto de servicios dejó de ser accesible desde otros hosts, mientras que algunos puertos publicados se comportaban de forma inconsistente.
El equipo pasó horas persiguiendo fantasmas: ¿era DNS? ¿era la red de overlay? ¿cambió el nombre del bridge?
La causa raíz fue más simple: con Docker sin gestionar iptables, las reglas de NAT y forward no se configuraban de forma fiable tras los reinicios,
y las reglas UFW específicas no recreaban la tubería de cadenas que Docker requiere.
Revirtieron el cambio y luego implementaron el punto de control correcto: DOCKER-USER para la política,
iptables gestionado por Docker para la mecánica. Esa división de responsabilidades es el compromiso sensato:
deja que Docker haga la plomería; tú decides qué está permitido a través de las tuberías.
3) La práctica aburrida pero correcta que salvó el día
Una plataforma de servicios financieros ejecutaba cargas en contenedores sobre imágenes Ubuntu endurecidas.
Nada sofisticado. Su arma secreta era una disciplina operativa aburrida: cada host tenía un trabajo estándar de “auditoría de exposición”.
Se ejecutaba cada noche, volcaba docker ps port mappings, ss -lntp listeners y una vista filtrada de iptables -S en un índice central de logs.
Una tarde, un desarrollador fusionó un cambio de Compose que accidentalmente publicó un endpoint de depuración en 0.0.0.0:6060.
El servicio no estaba “vulnerable” en sentido CVE; simplemente no debía ser alcanzable desde el mundo.
En menos de una hora, el diff de auditoría disparó una alerta: nuevo listener público, no estaba en la lista permitida.
El on-call no necesitó discutir con nadie. Tenía evidencia: un nuevo listener y una nueva regla de DNAT.
Revirtieron el cambio en Compose, redeployaron y la alerta se limpió.
Sin impacto al cliente. Sin parches de pánico. Sin reunión “¿cómo pasó esto?” que no produce más que invitaciones de calendario.
La lección no es “las auditorías son geniales”. La lección es que la verificación aburrida y repetida vence a la confianza puntual.
Esto es a lo que se parece el “trabajo de fiabilidad” cuando realmente funciona.
Errores comunes: síntomas → causa raíz → solución
1) “UFW deny incoming, pero mi puerto de contenedor sigue accesible”
Síntomas: Un escaneo externo alcanza HOST:published_port aunque UFW supuestamente lo bloquee.
Causa raíz: El tráfico se reenvía (cadena FORWARD) tras DNAT, no se entrega a INPUT. Las reglas de Docker lo aceptan.
Solución: Añade política en DOCKER-USER (drop por defecto desde la IF pública al bridge de Docker; permitir explícitamente). O enlaza el puerto a localhost/IP privada.
2) “Añadí reglas de UFW para bloquear el puerto y no cambió nada”
Síntomas: ufw deny 8080/tcp no afecta a los puertos publicados.
Causa raíz: El bloque aplica a INPUT; el tráfico se está DNATeando y reenviando.
Solución: Bloquea en DOCKER-USER o deja de publicar en 0.0.0.0.
3) “Todo se rompió después de activar un firewall estricto”
Síntomas: Los contenedores no pueden salir a Internet; falla la red entre contenedores; DNS dentro de contenedores es inestable.
Causa raíz: Reglas DROP demasiado amplias en FORWARD/DOCKER-USER sin permitir conexiones establecidas o rutas de salida necesarias.
Solución: Mantén la mecánica de reenvío de Docker, pero aplica política dirigida: permitir established/related, permitir tráfico necesario del bridge, luego descartar ingreso público a los bridges.
4) “Mis reglas funcionaban hasta el reinicio”
Síntomas: Tras el reinicio, los puertos vuelven a estar abiertos.
Causa raíz: Las reglas ad-hoc de iptables no se persistieron; Docker reconstruyó sus reglas; las tuyas desaparecieron.
Solución: Persiste reglas vía iptables-persistent/netfilter-persistent o codifícalas con gestión de configuración. Valida en reinicio con una prueba.
5) “Bloqueé la exposición del contenedor, pero el acceso localhost también falló”
Síntomas: El reverse proxy en el host no puede alcanzar backends en contenedores; fallan checks de salud.
Causa raíz: La regla drop en DOCKER-USER coincide demasiado amplio (descarta tráfico originado en el host o en interfaces internas).
Solución: Delimita reglas por interfaz (-i ens3 -o docker0) y/o por rangos de origen; mantiene permitido el tráfico host→bridge.
6) “UFW y Docker se pelean; reglas parecen duplicadas y raras”
Síntomas: Muchas cadenas; confusión; comportamiento inconsistente entre hosts.
Causa raíz: Mezcla de backends nftables/iptables, diferencias de distro o múltiples herramientas gestionando el estado del firewall.
Solución: Elige un plano de control. Verifica si usas iptables-nft de compatibilidad. Estandariza imágenes. Prueba la ruta real del paquete, no la intención.
Listas de verificación / plan paso a paso
Plan 1: Asegurar un host Docker existente de forma segura (apto para producción)
-
Inventario de exposición: lista de puertos publicados y listeners.
- Usa
docker psyss -lntp. - Decisión: ¿qué puertos deben ser públicos, privados o solo localhost?
- Usa
-
Identificar interfaces públicas: no adivines.
- Usa
ip -br addr. - Decisión: ¿qué interfaz(es) deberían poder alcanzar los contenedores?
- Usa
-
Confirmar orden de cadenas de Docker: asegúrate de que DOCKER-USER se referencia temprano.
- Usa
iptables -S FORWARD. - Decisión: si DOCKER-USER no está presente, estás en una configuración no estándar—arregla eso antes de continuar.
- Usa
-
Implementar una regla de drop estrecha para el ingreso público a los bridges de Docker.
- Empieza con un drop acotado por interfaz:
-i public -o docker0. - Decisión: añade allows encima para los puertos/orígenes requeridos.
- Empieza con un drop acotado por interfaz:
-
Probar desde fuera y vigilar contadores.
- Usa
nc -vzdesde otra máquina; revisa contadores de iptables. - Decisión: itera hasta que solo el acceso previsto tenga éxito.
- Usa
-
Persistir y automatizar.
- Usa iptables-persistent o gestión de configuración.
- Decisión: añade un gate en CI/CD o una auditoría nocturna para detectar nuevas exposiciones.
Plan 2: Construir hosts nuevos con “sin exposición accidental” por defecto
- Decide una estrategia de ingreso: un reverse proxy o un pequeño conjunto de puertos públicos.
- Exige direcciones de bind explícitas en Compose para todo lo que no deba ser público (
127.0.0.1:...). - Envía una política DOCKER-USER por defecto: drop desde IF pública al bridge; permitir solo los puertos del proxy.
- Añade un trabajo de auditoría de exposición (listeners + puertos publicados + diff de iptables).
- Prueba el comportamiento en reinicio: persistencia del firewall y orden de arranque de Docker.
Preguntas frecuentes
1) ¿Por qué “ufw deny 8080/tcp” no bloquea el puerto publicado por Docker?
Porque el tráfico se DNATea y se reenvía al contenedor. No se maneja por INPUT como lo haría un proceso del host.
Necesitas política en FORWARD/DOCKER-USER o eliminar el bind público.
2) ¿Docker está “evadiendo” UFW a propósito?
Docker programa iptables para implementar NAT y reenvío automáticamente. Eso puede contradecir tus expectativas, pero no es una función furtiva.
El hook pensado para administradores es DOCKER-USER.
3) ¿Debería desactivar la gestión de iptables de Docker?
Normalmente no. Si la desactivas, te responsabilizas de NAT, reenvío, aislamiento y casos límite tras reinicios.
Mantén la plomería de Docker; aplica tu política en DOCKER-USER y enlaza puertos con cuidado.
4) ¿Cuál es el conjunto de reglas DOCKER-USER más seguro por defecto?
Para hosts expuestos a Internet: permite puertos publicados explícitos desde orígenes explícitos, y luego descarta tráfico desde la(s) interfaz(es) pública(s) hacia los bridges de Docker.
Mantén el tráfico local del host e interno sin bloquear salvo que tengas razón para hacerlo.
5) ¿Puedo arreglar esto solo con UFW?
Puedes, pero es frágil a menos que entiendas profundamente cómo UFW se integra en FORWARD y cómo Docker inserta sus cadenas.
El camino más seguro operativamente es usar DOCKER-USER para la política de ingreso de contenedores, y UFW para INPUT del host.
6) ¿Por qué funciona tan bien el bind a 127.0.0.1?
Porque elimina la exposición en la capa de socket. No se necesitan maniobras del filtrado de paquetes.
Es la diferencia entre “bloqueado” y “no alcanzable”.
7) ¿Y el IPv6?
Si IPv6 está habilitado, debes aplicar política equivalente para ip6tables/nft.
De lo contrario “aseguras” IPv4 y accidentalmente dejas IPv6 completamente abierto. Audita ambas pilas.
8) ¿Por qué a veces veo docker-proxy escuchando y a veces no?
El comportamiento del proxy en space de usuario de Docker ha cambiado con el tiempo y puede activarse o desactivarse. Incluso sin docker-proxy,
iptables DNAT aún puede publicar puertos. Verifica siempre tanto los sockets como las reglas de iptables.
9) Si uso un contenedor reverse proxy, ¿aún necesito reglas DOCKER-USER?
Sí, si deseas salvaguardas. El proxy reduce la superficie, pero un -p accidental en un backend puede seguir exponiéndolo.
DOCKER-USER hace que ese error no sea fatal.
Próximos pasos que deberías hacer hoy
Deja de tratar “UFW activado” como un resultado de seguridad. En un host Docker, es una condición inicial.
Tu trabajo real es hacer la exposición deliberada: enlaza lo que pueda enlazarse a localhost y controla el resto en DOCKER-USER.
- Haz el inventario:
docker ps,ss -lntp, comprobación externa. - Inspecciona el orden de reglas: confirma que FORWARD salta a DOCKER-USER temprano.
- Implementa un drop por defecto desde interfaces públicas a bridges de Docker en DOCKER-USER, y luego permite solo lo que quieras.
- Persiste las reglas y prueba el comportamiento en reinicio.
- Añade una aburrida auditoría nocturna de exposición. Lo aburrido es lo que te mantiene empleado.