Docker ignora UFW: 4 soluciones probadas para tu VPS

12 min de lectura·Matthieu|

Docker manipula iptables directamente e ignora las reglas de UFW. Los puertos de tus contenedores están expuestos a Internet incluso con ufw deny activo. Aquí tienes cuatro soluciones con sus compromisos, cada una verificada escaneando desde un host externo.

Tu firewall UFW te está mintiendo. Si ejecutas Docker en un VPS con UFW activado, cada puerto publicado de un contenedor está completamente abierto a Internet. Ejecutar ufw deny 8080 no hace nada. Docker ignora UFW por completo.

Este tutorial muestra el problema en acción y luego detalla cuatro soluciones con diferentes compromisos. Cada solución incluye un paso de verificación: escanear el puerto desde un host externo para confirmar que realmente está bloqueado. No solo comprobar ufw status.

Esta guía funciona en Ubuntu 24.04 y Debian 12. Ambos usan la capa de compatibilidad iptables por defecto, por lo que las mismas soluciones aplican. Debian 12 usa nftables como backend por defecto, pero UFW y Docker interactúan a través de la interfaz iptables, que se mapea a nftables de forma transparente.

Requisitos previos: Un VPS con Docker instalado, UFW activado y acceso SSH. Una segunda máquina (tu portátil u otro servidor) para el escaneo de puertos externo.

¿Por qué Docker ignora las reglas del firewall UFW?

Docker escribe reglas iptables en las tablas nat y filter para enrutar tráfico hacia los contenedores. UFW gestiona solo la cadena INPUT. Cuando un paquete llega a un puerto publicado de un contenedor, las reglas NAT de Docker lo redirigen a través de la cadena FORWARD antes de que las reglas INPUT de UFW puedan verlo. El paquete nunca llega a UFW. Esto significa que ufw deny no tiene ningún efecto en los puertos publicados por Docker.

Este es el flujo de un paquete para una petición al puerto 8080 de tu VPS, donde un contenedor está publicado con -p 8080:80:

  1. El paquete llega a la interfaz de red
  2. Entra en la cadena PREROUTING de la tabla nat
  3. La regla DNAT de Docker reescribe el destino a la IP del contenedor (p. ej., 172.17.0.2:80)
  4. El paquete pasa a la cadena FORWARD de la tabla filter (no INPUT)
  5. La cadena DOCKER acepta el paquete reenviado
  6. El paquete llega al contenedor

UFW nunca lo ve porque solo vigila la cadena INPUT. El paquete toma la ruta FORWARD.

Esto no es un bug. Docker necesita el control de iptables para que funcione la red de contenedores. Pero crea una brecha de seguridad seria si asumías que UFW protegía tu servidor.

¿Cómo verifico que Docker está ignorando mi firewall UFW?

Antes de aplicar cualquier solución, comprueba el problema tú mismo. Inicia un contenedor de prueba que sirva HTTP en el puerto 8080:

docker run -d --name ufw-test -p 8080:80 nginx:alpine

Comprueba que UFW está activo y deniega tráfico entrante por defecto:

sudo ufw status verbose

Deberías ver Default: deny (incoming) en la salida. Ahora deniega explícitamente el puerto 8080:

sudo ufw deny 8080

Comprueba que UFW muestra el puerto como bloqueado:

sudo ufw status | grep 8080
8080                       DENY        Anywhere
8080 (v6)                  DENY        Anywhere (v6)

UFW reporta el puerto como denegado. Ahora prueba desde tu máquina local (no desde el servidor):

curl -s -o /dev/null -w "%{http_code}" http://YOUR_SERVER_IP:8080
200

La respuesta es 200. El contenedor es completamente accesible desde Internet a pesar de la regla de denegación de UFW. Si tienes nmap instalado en tu máquina local:

nmap -p 8080 YOUR_SERVER_IP
PORT     STATE SERVICE
8080/tcp open  http-proxy

El puerto está abierto. UFW no lo protege.

Elimina la regla deny (aplicarás una solución real a continuación):

sudo ufw delete deny 8080

Mantén el contenedor de prueba en ejecución. Lo usarás para verificar cada solución.

¿Qué solución Docker UFW debería usar?

Existen cuatro soluciones. Cada una tiene diferentes compromisos. Elige la que se adapte a tu configuración.

Solución Impacto en red Sobrevive reinicio Compatible con Compose Complejidad Ideal para
Cadena DOCKER-USER Ninguno Medio Control total, varios puertos públicos
Herramienta ufw-docker Ninguno Bajo Gestión automatizada, contenedores dinámicos
iptables=false Rompe la red entre contenedores, sin Internet desde contenedores Bajo Contenedores aislados individuales (raro)
Bind a 127.0.0.1 Ninguno Bajo Servicios detrás de un reverse proxy

Decisión rápida:

  1. ¿Todos tus contenedores están detrás de un reverse proxy (Nginx, Traefik, Caddy)? Usa binding a 127.0.0.1. Es lo más simple.
  2. ¿Necesitas contenedores accesibles públicamente en puertos específicos? Usa la cadena DOCKER-USER para control manual o ufw-docker para gestión automatizada.
  3. ¿Ejecutas contenedores que nunca deben acceder a la red? Usa iptables=false, pero entiende los compromisos.

¿Cómo corrijo el bypass de UFW por Docker con la cadena DOCKER-USER?

La cadena DOCKER-USER es un espacio reservado que Docker deja vacío para los administradores. Las reglas que añades aquí se evalúan antes de las propias reglas de reenvío de Docker. Al insertar reglas de integración con UFW en esta cadena a través de /etc/ufw/after.rules, haces que el tráfico Docker pase por UFW.

Este método te da control total. Funciona tanto con docker run como con Docker Compose. Sobrevive a reinicios porque las reglas se cargan al arrancar UFW.

Abre /etc/ufw/after.rules:

sudo cp /etc/ufw/after.rules /etc/ufw/after.rules.bak
sudo nano /etc/ufw/after.rules

Añade el siguiente bloque al final del archivo, después de la línea COMMIT existente:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -i docker0 -o docker0 -j ACCEPT

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 192.168.0.0/16

-A DOCKER-USER -j RETURN

-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP

COMMIT
# END UFW AND DOCKER

Qué hace este bloque:

  • -A DOCKER-USER -j ufw-user-forward: envía el tráfico Docker primero a las reglas de reenvío de UFW
  • conntrack --ctstate RELATED,ESTABLISHED: permite el tráfico de retorno de conexiones establecidas (mantiene las conexiones existentes tras cambios de reglas)
  • conntrack --ctstate INVALID: descarta paquetes malformados
  • -i docker0 -o docker0: permite la comunicación entre contenedores en la misma red bridge
  • Las líneas -j RETURN -s permiten tráfico originado desde subredes privadas
  • Las nuevas conexiones (ctstate NEW) hacia subredes Docker desde el exterior se registran y descartan
  • La regla de registro limita a 3 mensajes por minuto para evitar saturar los logs

Recarga UFW para aplicar las nuevas reglas:

sudo ufw reload

Verifica que las reglas están cargadas:

sudo iptables -L DOCKER-USER -n -v

Deberías ver tus nuevas reglas en la salida de la cadena. Si la cadena solo muestra una regla RETURN por defecto, el bloque after.rules no se cargó correctamente. Revisa la sintaxis del archivo.

Permitir un puerto de contenedor específico a través de UFW

Con la cadena DOCKER-USER activa, todos los puertos de contenedores están bloqueados por defecto desde el exterior. Para permitir un puerto específico, debes referenciar la IP del contenedor y el puerto del contenedor, no el puerto del host. Esto se debe a que la regla DNAT de Docker en la cadena PREROUTING reescribe el destino antes de que el paquete llegue a la cadena FORWARD. En el momento en que tu regla se evalúa, el destino es la dirección interna del contenedor.

Primero, encuentra la IP del contenedor:

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ufw-test
172.17.0.2

Luego permite el tráfico hacia ese contenedor:

sudo ufw route allow proto tcp from any to 172.17.0.2 port 80

Verifica que UFW muestra la regla de ruta:

sudo ufw status | grep "ALLOW FWD"
172.17.0.2 80/tcp          ALLOW FWD   Anywhere

Nota: como la regla apunta a una IP de contenedor que puede cambiar al reiniciar, usa la herramienta ufw-docker (siguiente sección) para el seguimiento automático de IPs. El enfoque manual DOCKER-USER es mejor cuando gestionas las IPs de contenedores tú mismo (IPs estáticas en Docker Compose o cuando automatizas las actualizaciones de reglas).

Verificación desde un host externo

Desde tu máquina local, prueba el puerto permitido:

curl -s -o /dev/null -w "%{http_code}" http://YOUR_SERVER_IP:8080
200

El puerto es accesible porque permitiste explícitamente el reenvío al contenedor. Ahora prueba un puerto no permitido (por ejemplo, si tuvieras un contenedor en el puerto 9090):

nmap -p 9090 YOUR_SERVER_IP
PORT     STATE    SERVICE
9090/tcp filtered unknown

filtered significa que el firewall descarta paquetes silenciosamente. La cadena DOCKER-USER funciona.

¿Cómo uso la herramienta ufw-docker para gestionar reglas de firewall de contenedores?

La herramienta chaifeng/ufw-docker automatiza la configuración de la cadena DOCKER-USER y proporciona comandos para permitir o denegar puertos de contenedores. Modifica /etc/ufw/after.rules por ti y añade un CLI para gestionar reglas por contenedor.

Instalación:

sudo wget -O /usr/local/bin/ufw-docker \
  https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker

Revisa el script antes de ejecutarlo. Es un script shell que puedes leer con cat /usr/local/bin/ufw-docker.

Ejecuta el instalador para parchear /etc/ufw/after.rules:

sudo ufw-docker install

Esto añade reglas de cadena DOCKER-USER similares a la sección anterior, incluyendo filtrado basado en conntrack y registro. También parchea after6.rules para IPv6. Recarga UFW:

sudo ufw reload

Verifica la instalación:

sudo ufw-docker check

Gestión de puertos de contenedores

Permitir acceso externo al puerto 80 de un contenedor llamado web:

sudo ufw-docker allow web 80/tcp

Listar reglas de un contenedor:

sudo ufw-docker list web

Eliminar una regla:

sudo ufw-docker delete allow web 80/tcp

Mostrar todas las reglas de firewall Docker:

sudo ufw-docker status

Ejemplo de Docker Compose con ufw-docker

services:
  web:
    image: nginx:alpine
    container_name: web
    ports:
      - "8080:80"
    restart: unless-stopped

Inicia el stack y luego permite el puerto:

docker compose up -d
sudo ufw-docker allow web 80/tcp

Nota: el comando ufw-docker referencia el puerto del contenedor (80), no el puerto del host (8080). La herramienta resuelve el mapeo automáticamente.

Alternativa: ufw-docker-automated

El proyecto shinebayar-g/ufw-docker-automated adopta un enfoque diferente. Se ejecuta como un contenedor Docker, escucha eventos de la API de Docker y crea reglas UFW automáticamente cuando los contenedores se inician o detienen. Esto resuelve una limitación de la herramienta ufw-docker: cuando un contenedor se reinicia y obtiene una nueva IP, la regla anterior se invalida.

Con ufw-docker-automated, añades etiquetas a tus contenedores:

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    labels:
      - "UFW_MANAGED=TRUE"
    restart: unless-stopped

La herramienta vigila contenedores con la etiqueta UFW_MANAGED=TRUE y crea reglas UFW correspondientes automáticamente. También soporta UFW_ALLOW_FROM para restringir el acceso a IPs o rangos CIDR específicos.

Verificación desde un host externo

Después de permitir un puerto con ufw-docker, confirma desde tu máquina local:

curl -s -o /dev/null -w "%{http_code}" http://YOUR_SERVER_IP:8080
200

Prueba un puerto no listado para confirmar que está bloqueado:

nmap -p 9090 YOUR_SERVER_IP
PORT     STATE    SERVICE
9090/tcp filtered unknown

¿Qué pasa si desactivo la gestión de iptables de Docker?

Configurar "iptables": false en la configuración del daemon de Docker impide que Docker cree reglas iptables. Es la solución más simple pero tiene los efectos secundarios más severos.

Qué deja de funcionar:

  • Los contenedores no pueden acceder a Internet (sin reglas de masquerading)
  • La comunicación entre contenedores en diferentes redes deja de funcionar
  • La publicación de puertos (-p) deja de funcionar por completo. Debes gestionar todas las reglas de reenvío tú mismo.

Este enfoque solo es apropiado si ejecutas contenedores aislados que no necesitan acceso a Internet y gestionas toda la red manualmente.

Edita o crea el archivo de configuración del daemon Docker:

sudo nano /etc/docker/daemon.json
{
  "iptables": false
}

Si el archivo ya tiene contenido, añade la clave "iptables": false al objeto JSON existente. No crees un segundo objeto JSON.

Reinicia Docker:

sudo systemctl restart docker

Verifica que Docker no está creando reglas iptables:

sudo iptables -L DOCKER -n 2>/dev/null

Con Docker 28.x, la cadena DOCKER puede seguir existiendo pero debería contener solo una regla DROP en lugar de las reglas de reenvío habituales. Ninguna regla ACCEPT significa que Docker no está enrutando tráfico hacia los contenedores.

Verificación desde un host externo

Reinicia el contenedor de prueba (se detuvo al reiniciar Docker):

docker start ufw-test

Desde tu máquina local:

nmap -p 8080 YOUR_SERVER_IP
PORT     STATE  SERVICE
8080/tcp closed http-proxy

closed (no filtered) porque Docker ya no crea las reglas NAT para reenviar tráfico. El puerto no está escuchando desde la perspectiva externa.

Para revertir, elimina "iptables": false de daemon.json y reinicia Docker:

sudo systemctl restart docker

¿Cómo vinculo los puertos de Docker Compose solo a localhost?

Para contenedores detrás de un reverse proxy (Nginx, Traefik, Caddy), la solución más simple es no publicar puertos en 0.0.0.0. Vincúlalos a 127.0.0.1 en su lugar. El reverse proxy se conecta al contenedor a través de localhost. El tráfico externo llega al reverse proxy en los puertos 80/443, que UFW controla normalmente.

Esto no requiere cambios en iptables, ni herramientas adicionales, y funciona en cualquier sistema operativo.

En tu docker-compose.yml, cambia el binding de puertos:

services:
  app:
    image: your-app:latest
    ports:
      - "127.0.0.1:8080:80"
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    restart: unless-stopped

El servicio app se vincula solo a 127.0.0.1:8080. No es accesible desde fuera del servidor. El servicio nginx se vincula a 0.0.0.0:80 y 0.0.0.0:443 (el valor por defecto cuando no se especifica IP), que controlas con UFW.

Para docker run, la sintaxis equivalente:

docker run -d --name app -p 127.0.0.1:8080:80 your-app:latest

Verificar el binding

En el servidor, confirma que el puerto está vinculado solo a localhost:

ss -tlnp | grep 8080
LISTEN 0      4096      127.0.0.1:8080      0.0.0.0:*    users:(("docker-proxy",pid=12345,fd=4))

Fíjate bien: la dirección de escucha es 127.0.0.1:8080, no 0.0.0.0:8080. Esto significa que solo se aceptan conexiones locales.

Verificación desde un host externo

Desde tu máquina local:

nmap -p 8080 YOUR_SERVER_IP
PORT     STATE  SERVICE
8080/tcp closed http-proxy

El puerto está cerrado al tráfico externo. Tu reverse proxy gestiona el acceso público en los puertos 80 y 443, que UFW protege normalmente.

Este es el enfoque recomendado para la mayoría de configuraciones VPS donde ejecutas aplicaciones web detrás de un reverse proxy.

Solución de problemas

Las reglas desaparecen tras reiniciar Docker

Si usaste el enfoque de la cadena DOCKER-USER y las reglas desaparecen al reiniciar Docker, verifica que tus reglas están en /etc/ufw/after.rules y no fueron añadidas manualmente con comandos iptables. Las reglas iptables manuales no sobreviven a reinicios de servicio. El archivo after.rules se carga cada vez que UFW arranca.

sudo ufw reload
sudo iptables -L DOCKER-USER -n -v

El contenedor no puede resolver DNS tras aplicar las reglas DOCKER-USER

El bloque after.rules incluye una regla conntrack --ctstate RELATED,ESTABLISHED que permite el tráfico de respuesta DNS de vuelta a los contenedores. Si la resolución DNS falla dentro de los contenedores, verifica que la regla conntrack está cargada:

sudo iptables -L DOCKER-USER -n -v | grep "RELATED,ESTABLISHED"

Si falta, revisa la sintaxis de tu bloque after.rules y recarga UFW.

Los comandos ufw-docker fallan con "container not found"

El comando ufw-docker allow requiere que el contenedor esté en ejecución. Resuelve el nombre del contenedor a una dirección IP. Si el contenedor está detenido, el comando falla. Inicia el contenedor primero, luego añade la regla.

Compatibilidad nftables en Debian 12

Debian 12 usa nftables como backend de firewall, pero UFW y Docker usan la capa de compatibilidad iptables (iptables-nft). Las soluciones de esta guía funcionan de forma idéntica en Debian 12 y Ubuntu 24.04. Verifica que estás usando el backend nft:

sudo iptables --version
iptables v1.8.10 (nf_tables)

El sufijo (nf_tables) confirma que la capa de compatibilidad iptables-nft está activa. Todas las reglas de la cadena DOCKER-USER funcionan a través de esta capa.

Revisar logs de tráfico bloqueado

Si una conexión se bloquea inesperadamente tras aplicar la solución de la cadena DOCKER-USER, revisa los logs de UFW Docker:

sudo journalctl -k | grep "UFW DOCKER BLOCK"

Las entradas de log muestran la IP de origen y el puerto de destino de los paquetes bloqueados.

Limpiar recursos de prueba

Elimina el contenedor de prueba cuando hayas terminado:

docker rm -f ufw-test

Si añadiste una regla UFW deny durante las pruebas, elimínala:

sudo ufw delete deny 8080

Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.

¿Listo para probarlo?

Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.

Ver planes VPS
Docker ignora UFW: 4 soluciones probadas en VPS