SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-23

Cómo evitar que Docker omita UFW y bloquear puertos

Docker usa reglas iptables que saltan UFW: aunque deniegues el puerto 8080, sigue accesible desde Internet. Entiende el bypass y aplica dos correcciones.

Por qué Docker omite UFW

Docker omite UFW porque los puertos publicados de los contenedores nunca pasan por las reglas del firewall que gestiona UFW. Cuando ejecuta docker run -p 8080:80, Docker escribe una regla DNAT (traducción de direcciones de red de destino) en la cadena PREROUTING de la tabla nat del kernel. Esa regla cambia el destino de cada paquete a la dirección privada del contenedor antes de que el kernel decida adónde debe dirigirlo. Después, el paquete modificado se reenvía al contenedor a través de la cadena FORWARD, que controla Docker. Las reglas de UFW están en la cadena INPUT, y el paquete nunca entra en ella. Por eso ufw status muestra la denegación predeterminada, sudo ufw deny 8080 informa de que la operación se realizó correctamente y el puerto 8080 sigue respondiendo a todo Internet.

Esto no es un error de Docker, y UFW no está averiado. Ambas herramientas programan el mismo firewall del kernel. Las reglas de Docker simplemente actúan antes en el recorrido del paquete, por lo que UFW nunca recibe la petición. Esta guía muestra el bypass, explica el mecanismo y después aborda las dos correcciones que funcionan: publicar los puertos en 127.0.0.1 y filtrar en la cadena DOCKER-USER. Si UFW es nuevo para usted, configúrelo primero con la guía básica del firewall UFW, porque un firewall con denegación predeterminada sigue siendo la base adecuada para todo lo demás en el servidor.

Verifique el bypass en su propio servidor

Empiece con un VPS donde UFW esté activo y tenga una política predeterminada de denegación 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 informe del firewall, el puerto está cerrado. Ahora pruebe desde otra máquina, no desde el propio servidor:

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

El contenedor responde. Añada una regla explícita de denegación y vuelva a probar:

sudo ufw deny 8080/tcp

El puerto sigue respondiendo porque la regla de denegación está en una cadena que el paquete nunca recorre. UFW no falló. Nunca llegó a consultarse. Por eso el problema también pasa tan desapercibido: no se muestra ningún error, el despliegue funciona y la salida del estado del firewall parece exactamente la de un servidor protegido y restringido.

El mecanismo: PREROUTING se ejecuta antes de INPUT

El kernel procesa los paquetes entrantes en un orden fijo, y todo el problema se debe a ese orden.

  1. PREROUTING se ejecuta primero. Las reglas de esta cadena pueden reescribir el destino del paquete, y la regla de Docker para un puerto publicado hace exactamente eso.
  2. A continuación se toma la decisión de enrutamiento. Un paquete dirigido al propio host pasa a la cadena INPUT. Un paquete dirigido a cualquier otra máquina pasa a la cadena FORWARD.
  3. Las reglas de UFW están en INPUT. Las reglas de Docker están en FORWARD.

Revise la regla de Docker correspondiente al 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 explica todo. El destino de cualquier paquete que llegue al puerto 8080 se reescribe como 172.17.0.2:80, la dirección del contenedor en la red bridge privada de Docker. Después de 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 hacia sus propias redes. Su regla deny 8080/tcp espera en INPUT un paquete que nunca llega.

En Ubuntu 24.04, el comando iptables actúa como interfaz sobre nftables, pero el orden de las cadenas y el resultado son idénticos. UFW y Docker escriben en la misma canalización de paquetes del kernel, y el punto de entrada de Docker es anterior. Esto no es exclusivo de UFW: firewalld en un VPS con Rocky o AlmaLinux filtra en el mismo punto de esa canalización y la misma regla DNAT evita ese filtrado, por lo que las correcciones siguientes también son las adecuadas en ese caso.

La solución habitual: publicar puertos en 127.0.0.1

La mayoría de los contenedores nunca tuvieron que ser públicos. Una base de datos, un servidor de aplicaciones detrás de un proxy inverso, un panel de administración o un endpoint de métricas no deberían 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 de Compose:

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

Esto funciona porque la regla DNAT de Docker ahora sólo coincide con los paquetes dirigidos a 127.0.0.1, y un paquete procedente de Internet no puede llevar legítimamente ese destino. Por tanto, el kernel lo descarta antes de que se ejecute cualquier regla del firewall. El puerto es accesible desde el host y desde ningún otro lugar. Compruebe la vinculación:

sudo ss -tlnp | grep 8080

La salida debe mostrar 127.0.0.1:8080, no 0.0.0.0:8080 ni [::]:8080. Después, 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 proxy inverso que controle los puertos 80 y 443 y distribuya las peticiones según el nombre de host. No publique ningún otro puerto. Ese es el patrón que se desarrolla en la guía del proxy inverso Traefik, y es la forma en que una aplicación autoalojada como Nextcloud en un VPS permanece inaccesible salvo a través de su proxy. La guía conceptos básicos de Docker Compose explica cómo se declaran las entradas ports: y el resto del flujo de trabajo de Compose.

Con todos los contenedores internos vinculados a loopback, UFW vuelve a cumplir su función normal: proteger los puertos que sirve el propio host. Cree aquí ese conjunto de reglas y ejecute los comandos en orden:

ToolUFW rule generator

Filtrado real: la cadena DOCKER-USER

A veces un puerto de contenedor debe seguir publicado en la red, pero con acceso restringido. Por ejemplo, el puerto de una réplica de base de datos puede estar disponible sólo para la dirección de una oficina. Para eso, Docker proporciona la cadena DOCKER-USER. Cada paquete dirigido a cualquier contenedor pasa por DOCKER-USER antes de llegar a las reglas de aceptación propias de Docker, y Docker nunca escribe reglas en ella. La cadena existe para que la administre usted, y Docker no modifica su contenido cuando se reinicia el daemon.

Antes de ejecutar el comando, tenga en cuenta una particularidad. Cuando un paquete llega a DOCKER-USER, la reescritura DNAT ya se ha realizado. El puerto de destino del paquete es el puerto del contenedor (80 en nuestro ejemplo), no el puerto publicado (8080). Por tanto, una regla que coincida con --dport 8080 no coincide con ningún paquete. El método fiable consiste en buscar el puerto al que se conectó originalmente el cliente, que el seguimiento de conexiones del kernel conserva:

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

La regla se interpreta así: para los paquetes que entraron por eth0 y pertenecen a una conexión cuyo puerto de destino original era 8080, descartar todo lo que no proceda de 10.0.0.10. La coincidencia --ctdir ORIGINAL limita la regla a la dirección cliente-contenedor, por lo que las respuestas no quedan bloqueadas por error. Sustituya eth0 por su interfaz pública; ip route | grep default muestra su nombre. Pruébelo igual que antes: curl desde la dirección permitida debe funcionar, mientras que desde cualquier otra dirección la conexión agota el tiempo de espera. Esa espera es la señal de que una regla DROP está funcionando, no de que no haya ningún servicio detrás del puerto. La diferencia entre una conexión rechazada y otra que agota el tiempo de espera es la forma más rápida de distinguir un puerto filtrado de un servicio que simplemente no está escuchando.

Las reglas añadidas con el comando iptables desaparecen al reiniciar el sistema. Como UFW ya administra este firewall, el lugar adecuado para conservarlas 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

A continuación, ejecute sudo ufw reload. UFW vuelve a aplicar ese archivo en cada recarga y en cada arranque. Así, el filtrado de los contenedores queda en el mismo lugar que el resto del firewall y sobrevive tanto a un reinicio como a una actualización de Docker.

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

Las respuestas antiguas a este problema sugieren establecer { "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 permite que los contenedores accedan a Internet mediante la dirección del host. Por tanto, con la integración deshabilitada, los contenedores no pueden descargar imágenes, acceder a mirrors de paquetes ni llamar a ninguna API externa (interfaz de programación de aplicaciones). Las reglas DNAT son las que hacen funcionar -p, por lo que los puertos publicados dejan de funcionar por completo. Las reglas de aislamiento que mantienen separadas las redes de Compose también desaparecen. Resolvería el bypass rompiendo la conectividad de red de los contenedores, y tendría que escribir y mantener manualmente cada una de esas reglas. La documentación de Docker describe esta opción como destinada a quienes pretenden hacer exactamente eso. La cadena DOCKER-USER existe precisamente para que nadie necesite activar esta opción.

El aspecto IPv6 del mismo problema

Primero compruebe cómo aparece el puerto publicado en IPv6:

sudo ss -tlnp | grep 8080

Desde Docker Engine 27, Docker gestiona ip6tables de forma predeterminada. En una red Docker con IPv6 habilitado, un puerto publicado recibe el mismo tratamiento DNAT en las tablas IPv6. Por tanto, allí también existe el mismo bypass y se aplica la misma corrección: la cadena DOCKER-USER también existe en ip6tables, así que replique la 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 en espacio de usuario que escucha en [::]:8080 y reenvía el tráfico al contenedor mediante IPv4. El tráfico dirigido a un proceso del host sí pasa por INPUT, por lo que UFW puede filtrar esa ruta, pero sólo cuando UFW gestiona IPv6. El manual de UFW e IPv6 explica si lo gestiona y las otras formas en que puede aparecer una brecha de IPv6 en un VPS.

Publicar en loopback evita toda la cuestión: -p 127.0.0.1:8080:80 se enlaza sólo con loopback IPv4, por lo que no hay ningún listener IPv6 ni nada accesible desde fuera en ninguna de las dos pilas.

El patrón que se mantiene

  • Publique todos los puertos internos en 127.0.0.1 para que nunca queden expuestos desde el principio.
  • Asigne la parte pública a un único reverse proxy que gestione los puertos 80 y 443.
  • Mantenga UFW con la política predeterminada de denegación para el host y permita SSH y los puertos del proxy.
  • Filtre los puertos de contenedor realmente públicos en DOCKER-USER, según el puerto de destino original, y persista las reglas en /etc/ufw/after.rules.
  • Mantenga activada la integración de Docker con iptables.

Una vez configurado, este esquema elimina las sorpresas: ufw status describe el host y DOCKER-USER describe los contenedores. Nada se publica por accidente y el siguiente docker run -p que escriba expone exactamente lo que pretendía.

FAQ

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

Docker publica el puerto mediante una regla DNAT en la cadena PREROUTING. Esta regla cambia el destino del paquete por la dirección del contenedor antes de que se aplique cualquier filtrado. A continuación, el paquete sigue la ruta FORWARD, mientras que las reglas de UFW están en INPUT, una cadena por la que el paquete nunca pasa. El firewall nunca interviene, por lo que sus reglas de denegación no afectan a los puertos publicados de los contenedores.

¿Cómo hago que UFW bloquee los puertos publicados de 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, de modo que sólo el host pueda acceder a él. Otra opción es filtrar en la cadena DOCKER-USER mediante una regla de iptables que coincida con el puerto de destino original usando conntrack. Guarde esa regla en /etc/ufw/after.rules para que persista tras los reinicios y ufw reload.

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

No. Esta configuración elimina todas las reglas de firewall y NAT de Docker, lo que rompe mucho más que el bypass. Los contenedores pierden el acceso saliente a Internet porque desaparece la regla de masquerade, y los puertos publicados dejan de funcionar porque desaparecen las reglas DNAT. Use la publicación en loopback y la cadena DOCKER-USER. Estas opciones corrigen la exposición sin romper la conectividad de los contenedores.

¿Docker evita UFW también en IPv6?

En Docker Engine 27 y versiones posteriores, la gestión de ip6tables está activada de forma predeterminada. Por tanto, un puerto publicado en una red Docker con IPv6 habilitado se reescribe evitando UFW, igual que en IPv4, y necesita la misma regla DOCKER-USER reflejada con ip6tables. En las redes sin IPv6, el proceso docker-proxy escucha en [::], y ese tráfico sí pasa por INPUT. UFW puede filtrarlo si gestiona IPv6. La publicación en 127.0.0.1 evita ambos casos porque no hay ningún proceso escuchando en IPv6.