SSD Nodes Learn
Guías Matt ConnorPor Matt Connor · Actualizado 2026-07-24

Docker ignora reglas de UFW y cómo solucionarlo

Docker inserta reglas DNAT en iptables que saltan las reglas de UFW. Aprende por qué los puertos siguen abiertos y cómo corregirlo con soluciones efectivas.

Por qué Docker omite UFW

Docker omite UFW porque los puertos de contenedores publicados nunca pasan por las reglas de firewall que gestiona UFW. Cuando ejecutas docker run -p 8080:80, Docker escribe una regla DNAT (destination network address translation) en la cadena PREROUTING de la tabla nat del kernel. Esa regla reescribe el destino de cada paquete hacia la dirección privada del contenedor antes de que el kernel decida el destino del paquete. El paquete reescrito se reenvía al contenedor a través de la cadena FORWARD, la cual es controlada por Docker. Las reglas de UFW residen en la cadena INPUT y el paquete nunca entra en ella. Por lo tanto, ufw status muestra denegación por defecto, sudo ufw deny 8080 reporta éxito y el puerto 8080 sigue respondiendo a todo internet.

Esto no es un error de Docker y UFW no está fallando. Ambas herramientas programan el mismo firewall del kernel. Las reglas de Docker actúan en un punto anterior de la ruta del paquete, por lo que nunca se consulta a UFW. Esta guía demuestra la omisión, explica el mecanismo y cubre las dos soluciones efectivas: publicar puertos en 127.0.0.1 y filtrar en la cadena DOCKER-USER. Si UFW es nuevo para ti, configúralo primero con la guía básica de UFW, ya que un firewall con denegación por defecto sigue siendo la base correcta para el resto de servicios en el servidor.

Compruebe el bypass en su propio servidor

Comience desde un VPS con UFW activo y una política de denegación predeterminada para el tráfico entrante. Ejecute un contenedor web con un puerto publicado:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose muestra Default: deny (incoming), allow (outgoing) y ninguna regla para el puerto 8080. Según el reporte del propio firewall, el puerto está cerrado. Ahora realice la prueba desde una máquina distinta, no desde el propio servidor:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

El contenedor responde. Añada una regla de denegación explícita y pruebe de nuevo:

sudo ufw deny 8080/tcp

El puerto sigue respondiendo porque la regla de denegación se encuentra en una cadena que el paquete nunca visita. UFW no falló. Nunca se consultó. Esta es también la razón por la que el problema es tan difícil de detectar: no se imprime ningún error, el despliegue funciona y la salida del estado del firewall parece la de un servidor bloqueado y saludable.

El mecanismo: PREROUTING se ejecuta antes que INPUT

El kernel procesa los paquetes entrantes en un orden fijo, y el problema reside en ese orden.

  1. PREROUTING se ejecuta primero. Las reglas aquí pueden reescribir el destino del paquete; la regla de Docker para un puerto publicado hace precisamente eso.
  2. El siguiente paso es la decisión de enrutamiento. Un paquete dirigido al host va a la cadena INPUT. Un paquete dirigido a cualquier otra máquina va a la cadena FORWARD.
  3. Las reglas de UFW están en INPUT. Las reglas de Docker están en FORWARD.

Observe la regla de Docker para el contenedor que acaba de iniciar:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

La línea DNAT lo explica todo. Cualquier paquete que llegue al puerto 8080 tiene su destino reescrito a 172.17.0.2:80, la dirección del contenedor en la red bridge privada de Docker. Tras la reescritura, el paquete ya no está dirigido al host, por lo que la decisión de enrutamiento lo envía por la ruta FORWARD, donde Docker ya ha añadido reglas que aceptan tráfico en sus propias redes. Su regla deny 8080/tcp espera en INPUT un paquete que nunca llega.

En Ubuntu 24.04, el comando iptables es una interfaz para nftables, pero el orden de las cadenas y el resultado son idénticos. Tanto UFW como Docker escriben en el mismo pipeline de paquetes del kernel, y el punto de entrada de Docker es anterior.

La solución habitual: publicar puertos en 127.0.0.1

La mayoría de los contenedores no necesitan ser públicos. Una base de datos, un servidor de aplicaciones detrás de un reverse proxy, un panel de administración o un endpoint de métricas no deben responder directamente a internet. Publíquelos en la dirección de loopback:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

O en un archivo Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

Esto funciona porque la regla DNAT de Docker ahora solo coincide con paquetes dirigidos a 127.0.0.1. Un paquete desde internet nunca puede tener ese destino de forma legítima, por lo que el kernel lo descarta antes de que se ejecute cualquier regla de firewall. El puerto es accesible desde el host y desde ningún otro lugar. Verifique el binding:

sudo ss -tlnp | grep 8080

Debe ver 127.0.0.1:8080 en la salida, no 0.0.0.0:8080 ni [::]:8080. Luego, confirme desde otra máquina que curl http://your-vps-ip:8080/ es rechazado.

Para los servicios que deben estar expuestos a internet, ejecute un único reverse proxy que gestione los puertos 80 y 443 y rote por hostname; no publique nada más. Ese es el patrón que establece la guía del reverse proxy Traefik, y es como una aplicación self-hosted como Nextcloud en un VPS permanece inaccesible excepto a través de su proxy. La declaración de las entradas ports: y el resto del flujo de trabajo de Compose se explican en la guía de conceptos básicos de Docker Compose.

Al tener cada contenedor interno en loopback, UFW vuelve a su función normal: proteger los puertos que el propio host sirve. Configure ese conjunto de reglas aquí y luego ejecute los comandos en orden:

ToolUFW rule generator

Filtrado real: la cadena DOCKER-USER

A veces, un puerto de un contenedor debe permanecer publicado en la red pero restringido; por ejemplo, un puerto de réplica de base de datos que solo una dirección de oficina específica puede alcanzar. Para ello, Docker proporciona la cadena DOCKER-USER. Cada paquete destinado a cualquier contenedor pasa por DOCKER-USER antes de las reglas de aceptación de Docker, y Docker nunca escribe reglas en ella. La cadena existe para el usuario y Docker no modifica su contenido al reiniciar el daemon.

Una advertencia antes del comando: para cuando un paquete llega a DOCKER-USER, el reescrito DNAT ya ha ocurrido. El puerto de destino del paquete es el puerto del contenedor (80 en nuestro ejemplo), no el puerto publicado (8080). Por lo tanto, una regla que coincida con --dport 8080 no coincidirá con nada. La forma fiable es coincidir con el puerto que el cliente marcó originalmente, el cual el connection tracker del kernel recuerda:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

Léase como: para paquetes que entraron por eth0 y pertenecen a una conexión cuyo puerto de destino original era 8080, descartar todo lo que no provenga de 10.0.0.10. La coincidencia --ctdir ORIGINAL limita la regla a la dirección cliente-a-contenedor, evitando que los paquetes de respuesta se descarten por error. Reemplace eth0 con su interfaz pública; ip route | grep default la identifica. Realice la prueba de la misma manera que antes: curl desde la dirección permitida tiene éxito, y desde cualquier otro lugar la conexión agota el tiempo de espera.

Las reglas añadidas con el comando iptables desaparecen al reiniciar. Dado que UFW ya gestiona este firewall, el lugar adecuado para persistirlas es /etc/ufw/after.rules. Añada un bloque al final del archivo:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

Luego ejecute sudo ufw reload. UFW vuelve a aplicar ese archivo en cada recarga y en cada arranque, por lo que su filtrado de contenedores ahora reside en el mismo lugar que el resto de su firewall, sobreviviendo tanto a un reinicio como a una actualización de Docker.

Por qué no debe desactivar la integración de iptables de Docker

Las respuestas antiguas a este problema sugieren configurar { "iptables": false } en /etc/docker/daemon.json. No lo haga. Las reglas de firewall de Docker hacen mucho más que publicar puertos. La regla de masquerade es la que permite el acceso de los contenedores a internet mediante la dirección del host; si desactiva la integración, los contenedores no podrán descargar imágenes, acceder a repositorios de paquetes ni llamar a ninguna API externa. Las reglas DNAT son las que permiten el funcionamiento de -p, por lo que los puertos publicados dejarán de funcionar por completo. Las reglas de aislamiento que mantienen separadas las redes de Compose también desaparecerán. Intentaría solucionar el bypass rompiendo la red de los contenedores, y tendría que escribir y mantener cada una de esas reglas manualmente. La documentación oficial de Docker describe este ajuste para usuarios que pretenden hacer exactamente eso. La cadena DOCKER-USER existe precisamente para que nadie necesite este interruptor.

El lado de IPv6 del mismo problema

Primero verifique cómo se ve el puerto publicado en IPv6:

sudo ss -tlnp | grep 8080

Desde Docker Engine 27, Docker gestiona ip6tables de forma predeterminada. En una red de Docker con IPv6 habilitado, un puerto publicado recibe el mismo tratamiento DNAT en las tablas de IPv6. Por lo tanto, existe el mismo bypass y se aplica la misma solución: la cadena DOCKER-USER también existe en ip6tables, así que replique su regla con sudo ip6tables -I DOCKER-USER ... y pruebe desde fuera con curl contra la dirección IPv6 pública de su servidor, por ejemplo curl -6 http://[2001:db8:2a::1]:8080/.

En una red sin IPv6, los clientes IPv6 son gestionados por docker-proxy, un proceso normal de espacio de usuario que escucha en [::]:8080 y reenvía el tráfico al contenedor mediante IPv4. El tráfico hacia un proceso del host sí pasa por INPUT, por lo que UFW puede filtrar esa ruta, pero solo cuando UFW está gestionando IPv6. Si esto ocurre, y otras formas en que se abre una brecha de IPv6 en un VPS, es el tema de la guía de UFW e IPv6.

Publicar en loopback evita todo el problema: -p 127.0.0.1:8080:80 solo vincula el loopback de IPv4, por lo que no hay un listener de IPv6 y no hay nada que alcanzar desde fuera en ninguna de las pilas.

El patrón de arquitectura

  • Publique cada puerto interno en 127.0.0.1 para evitar que queden expuestos.
  • Asigne la parte pública a un único reverse proxy que gestione los puertos 80 y 443.
  • Mantenga la política por defecto de UFW en deny para el host, permitiendo solo SSH y los puertos del proxy.
  • Filtre los puertos de contenedores realmente públicos en DOCKER-USER, basándose en el puerto de destino original y persistido en /etc/ufw/after.rules.
  • Mantenga activada la integración de iptables de Docker.

Configurado una vez, esto elimina imprevistos: ufw status describe el host y DOCKER-USER describe los contenedores. Nada se publica por accidente, y el próximo docker run -p que escriba expondrá exactamente lo que usted desea.

FAQ

¿Por qué puedo acceder a mi contenedor Docker si UFW bloquea el puerto?

Porque Docker publica el puerto mediante una regla DNAT en la cadena PREROUTING, lo que reescribe el destino del paquete hacia la dirección del contenedor antes de cualquier filtrado. El paquete sigue la ruta FORWARD, mientras que las reglas de UFW se encuentran en INPUT, una cadena a la que el paquete nunca entra. El firewall no es consultado, por lo que sus reglas de denegación no tienen efecto en los puertos publicados de los contenedores.

¿Cómo hago que UFW bloquee los puertos publicados por Docker?

UFW no puede hacerlo directamente porque sus reglas están en la cadena incorrecta. Puede dejar de exponer el puerto, publicándolo como 127.0.0.1:8080:80 para que solo el host tenga acceso, o filtrar en la cadena DOCKER-USER mediante una regla de iptables que coincida con el puerto de destino original a través de conntrack. Guarde esa regla en /etc/ufw/after.rules para que persista tras reinicios y ufw reload.

¿Debo configurar "iptables": false en el daemon.json de Docker?

No. Esa configuración elimina todas las reglas de firewall y NAT de Docker, lo que causa más problemas que el bypass original. Los contenedores pierden el acceso a internet porque se elimina la regla de masquerade, y los puertos publicados dejan de funcionar porque se eliminan las reglas DNAT. Use la publicación en loopback y la cadena DOCKER-USER en su lugar; esto corrige la exposición sin romper la red de los contenedores.

¿Docker también salta UFW en IPv6?

En Docker Engine 27 y versiones posteriores, la gestión de ip6tables está activada por defecto. Un puerto publicado en una red Docker con IPv6 es reescrito saltando UFW exactamente igual que en IPv4, y requiere que la misma regla DOCKER-USER se replique con ip6tables. En redes sin IPv6, el proceso docker-proxy escucha en [::] y ese tráfico sí pasa por INPUT, donde UFW puede filtrarlo si gestiona IPv6. Publicar en 127.0.0.1 evita ambos casos, ya que no hay nada escuchando en IPv6.