Problema
En muchos servidores domésticos o de pequeña empresa Docker se ejecuta detrás de un firewall gestionado con UFW. La expectativa natural es que ufw status refleje exactamente qué puertos están permitidos o bloqueados. Sin embargo, Docker manipula directamente iptables, insertando reglas de NAT y de filtrado que pueden estar por encima de las cadenas creadas por UFW. Cuando eso ocurre, un puerto publicado con -p host:container queda accesible desde la red aunque ufw status indique que está bloqueado. El síntoma típico es:
ufw statusmuestra el puerto como DENY.- Desde otra máquina del LAN el servicio responde (por ejemplo, Nginx en
8080). - Un
curl localhost:8080en el host también funciona, lo que da la falsa impresión de que todo está bajo control.
Este desajuste rompe la confianza en el firewall y expone servicios sin que el administrador lo sepa.
Causa
El comportamiento se debe a tres factores que aparecen con frecuencia:
-
Orden de las cadenas en la tabla
filter
Docker crea la cadenaDOCKER-USERy la inserta muy temprano (regla 2) en la cadenaFORWARD. Las reglas de UFW se añaden mucho más abajo (a partir de la regla 8). Cuando un paquete llega, la regla de DNAT en la tablanatya lo ha redirigido al contenedor antes de que UFW tenga oportunidad de evaluarlo. -
DNAT antes del filtrado
La regla-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKERen la tablanatcambia la dirección de destino al IP interno del contenedor. Después de esa transformación el paquete ya no coincide con los criterios de puerto que UFW inspecciona (p.ej.,--dport 8080), por lo que la regla de denegación nunca se dispara. -
Publicación sin dirección explícita
La sintaxis-p 8080:80equivale a-p 0.0.0.0:8080:80, es decir, el puerto queda escuchando en todas las interfaces. Si la regla de UFW solo permite tráfico en una interfaz concreta (por ejemplo,eth0), el tráfico que llega aloo a otra interfaz puede pasar sin ser filtrado.
Estas tres piezas crean la ilusión de que UFW está “mintiendo”, cuando en realidad simplemente no está en la posición adecuada del flujo de paquetes.
Solución
La solución consiste en alinear el punto de inspección del firewall con el momento en que Docker ya ha hecho el DNAT, o bien evitar que Docker publique el puerto en interfaces no deseadas. Se pueden aplicar dos enfoques complementarios:
1. Añadir reglas a DOCKER-USER
DOCKER-USER es la cadena diseñada para que el administrador inserte filtros que se ejecuten después del DNAT pero antes de que el paquete sea reenviado al contenedor. Una regla típica para bloquear un puerto publicado sería:
iptables -I DOCKER-USER -p tcp --dport 8080 -j REJECT --reject-with tcp-reset
Esta regla se coloca al inicio de DOCKER-USER (-I), por lo que cualquier paquete que haya sido NATeado a 8080 será rechazado inmediatamente, independientemente de lo que muestre ufw status.
2. Publicar solo en la interfaz deseada
Al lanzar el contenedor, especificar la dirección de enlace:
docker run -d -p 127.0.0.1:8080:80 nginx:alpine
Con 127.0.0.1 el puerto queda escuchando únicamente en el loopback, lo que elimina la necesidad de filtrar tráfico externo. Si se necesita exposición externa pero bajo control, se puede usar la IP de la interfaz de red (-p 192.168.1.10:8080:80).
3. Sincronizar UFW con Docker mediante reglas persistentes
Una alternativa más estructurada es crear una regla de UFW que se inserte en la cadena DOCKER-USER automáticamente. UFW permite añadir “raw” rules:
ufw route insert 1 deny in on eth0 to any port 8080 proto tcp comment 'Bloquear Docker 8080'
El route insert 1 coloca la regla al principio de la cadena de rutas, que UFW traduce a una regla en DOCKER-USER. De esta forma se mantiene la gestión centralizada en UFW sin perder efectividad.
Cuándo aplicar esta solución
Aplica cuando:
- Se usan contenedores que publican puertos en la máquina host.
- UFW está activo y configurado con política
denypor defecto. ufw statusmuestra puertos como “DENY” pero el servicio responde desde otra máquina.- Se necesita un control fino de qué puertos pueden ser expuestos, sin desactivar la integración automática de Docker con iptables.
No es necesario si:
- Docker se ejecuta en modo
--network=noneo con redesbridgesin publicar puertos. - El firewall se gestiona exclusivamente con herramientas que manipulan directamente
iptables(p.ej., firewalld) y se ha configurado el orden de cadenas adecuadamente. - La exposición de puertos se limita a
localhosty no hay riesgo de acceso externo.
Código
# 1. Bloquear puerto 8080 en la cadena DOCKER-USER
iptables -I DOCKER-USER -p tcp --dport 8080 -j REJECT --reject-with tcp-reset
# 2. Lanzar contenedor escuchando solo en loopback
docker run -d -p 127.0.0.1:8080:80 --name nginx_local nginx:alpine
# 3. Añadir regla UFW que se refleje en DOCKER-USER
ufw route insert 1 deny in on eth0 to any port 8080 proto tcp comment 'Bloquear Docker 8080'
Verificación
-
Desde otra máquina en la misma LAN, intenta conectar:
curl http://<host-ip>:8080Debería recibir connection refused o timeout según la regla elegida.
-
En el host, verifica que el puerto sigue escuchando:
ss -ltnp | grep 8080La salida mostrará
nginx(o el proceso) escuchando en127.0.0.1:8080si se usó la opción de enlace local. -
Revisa las reglas:
iptables -L DOCKER-USER -n -v ufw status numberedConfirma que la regla de bloqueo aparece en
DOCKER-USERy que UFW muestra la regla correspondiente. -
Prueba de rebote: elimina temporalmente la regla de
DOCKER-USERy vuelve a probar la conexión externa para asegurarte de que la causa era efectivamente esa regla.
Notas adicionales
- Las reglas en
DOCKER-USERpersisten solo mientras el kernel está activo. Para que sobrevivan a reinicios, añádelas a un script de arranque (por ejemplo,/etc/iptables/rules.v4) o usa la funcionalidad de “persistencia” deiptables-persistent. - Si gestionas varios hosts con Ansible o similar, incluye la inserción de la regla
DOCKER-USERcomo una tarea idempotente; de esa forma mantienes la consistencia sin depender de la salida deufw status. - En entornos con Docker Compose, la opción
ports:permite especificar la dirección de enlace (127.0.0.1:8080:80). Añadirla al archivodocker-compose.ymlevita la necesidad de reglas manuales. - Recuerda que la tabla
natsiempre se evalúa antes defilter. Cambiar el orden de las cadenas no es posible, por lo que la única vía fiable es actuar después del DNAT (cadenaDOCKER-USER) o limitar la exposición en la fase de publicación. - Si utilizas Docker Swarm o Kubernetes, el mismo principio se aplica: los componentes de red (ingress, kube-proxy) realizan DNAT antes de que los firewalls de host inspeccionen el tráfico. En esos casos, la regla de filtrado debe insertarse en la cadena correspondiente del CNI (por ejemplo,
KUBE-FIREWALL).