Configuraste un firewall nftables desde cero en Debian 13. Lo pruebas. Te sientes bien. Luego instalas Docker y —misteriosamente— aparecen puertos abiertos, el reenvío se comporta de forma distinta y los paquetes empiezan a tomar rutas que no autorizaste.
Esto es una de esas molestias operacionales que pueden convertirse en un incidente de seguridad si sigues tratándolo como “solo cosas de red”. Docker tiene sus preferencias. nftables tiene las suyas. Tu equipo de cumplimiento tiene las suyas. Solo uno de ellos te paga el salario.
El modelo mental: ¿quién es el dueño del firewall?
En Debian 13, nftables es la interfaz de firewall de primera clase, pero el kernel sigue exponiendo hooks de netfilter que múltiples herramientas pueden programar. Docker, por defecto, también programa esos hooks. Dependiendo del empaquetado y la configuración, puede hacerlo a través de iptables-nft (sintaxis de iptables, backend nft) mientras tú escribes reglas nft nativas. Mismos hooks, herramientas distintas, estado compartido. Así es como terminas con un firewall “funcional” que se comporta como un espectáculo de improvisación.
El mayor error es pensar en términos de “mi archivo de reglas del firewall”. Al sistema no le importa tu archivo. Al sistema le importa el conjunto de reglas activo en el kernel. Docker modifica ese conjunto activo dinámicamente. Si quieres previsibilidad, debes decidir:
- O dejar que Docker gestione NAT y el reenvío básico, y que tú lo controles mediante los puntos de estrangulamiento correctos (especialmente el hook
DOCKER-USER), - O desactivar la intervención de Docker en el firewall y asumir la responsabilidad total del NAT/reenvío/puertos publicados tú mismo.
El punto intermedio es donde los equipos de seguridad van a llorar.
Una cita que ha sido pegada en más de un portátil de guardia:
La esperanza no es una estrategia.
— General Gordon R. Sullivan
Nueve hechos que te evitarán adivinar
- nftables reemplazó a iptables como “sucesor” hace años, pero iptables sigue usándose ampliamente como interfaz de compatibilidad, a menudo mapeando a nft detrás de escena.
- Docker históricamente usó iptables directamente para configurar NAT y la publicación de puertos. Ese legado aún aparece hoy, incluso cuando nftables es tu interfaz elegida.
- El backend
iptables-nftsignifica que los comandos iptables pueden crear reglas nftables. Eso es conveniente, pero también significa que dos sintaxis distintas escriben en un mismo conjunto de reglas. - Los “puertos publicados” de Docker no son solo sockets en escucha; típicamente son reglas DNAT más cambios en el filtrado. Un contenedor puede ser alcanzable aunque nada esté ligado en el host como esperas.
- La cadena
DOCKER-USERexiste específicamente para permitir que los operadores apliquen política antes de las reglas accept propias de Docker. Si no la usas, estás dejando control (y dinero) sobre la mesa. - NAT y filtrado son tablas diferentes. Bloquear en
filtersin entendernatpuede llevar a “está bloqueado pero de alguna forma funciona” o “está abierto pero no es accesible” y mucha confusión. - El reenvío depende de
net.ipv4.ip_forwardy sysctls relacionados; Docker puede habilitar comportamientos que tu endurecimiento básico deshabilitó, y viceversa. - Los firewalls se evalúan en orden de hooks. Si Docker inserta reglas antes que las tuyas (o con mayor prioridad), tus drops cuidadosamente diseñados pueden no ejecutarse nunca.
- Los valores por defecto de Debian pueden ser engañosamente silenciosos: puedes tener “nftables instalado” pero “servicio nftables inactivo”, y aun así tener reglas activas insertadas por otros componentes.
Chiste #1: Los firewalls son como la política de oficina: todo está bien hasta que alguien cambia silenciosamente la cadena de mando.
Qué ocurre realmente en Debian 13 cuando Docker arranca
1) Docker crea bridges y namespaces
Docker típicamente crea un bridge de Linux (comúnmente docker0) y uno o varios bridges definidos por el usuario. Los contenedores están en namespaces de red con pares veth conectados a esos bridges. Esta parte es directa.
2) Docker programa netfilter para que “simplemente funcione”
“Simplemente funcionar” significa:
- Los contenedores pueden llegar a Internet mediante masquerading (SNAT) en el egress.
- Los puertos publicados en el host se reenvían (DNAT) a las IPs de los contenedores.
- El reenvío entre interfaces del host y el bridge de contenedores se acepta.
La implementación exacta depende de si Docker usa la compatibilidad de iptables y de si el sistema ejecuta iptables heredado o iptables con backend nft. En Debian 13, debes asumir backend nft a menos que explícitamente forzaras el legado.
3) Verás cadenas nftables que no escribiste
Los artefactos típicos incluyen cadenas como DOCKER, DOCKER-USER y a veces reglas en nat para DNAT/MASQUERADE. Los nombres exactos pueden variar; el patrón no. Si te sorprenden, no estás solo. Pero “sorprendido” no es un estado aceptable en producción.
4) La “sorpresa” generalmente no es que Docker sea malicioso
Es Docker optimizando la experiencia de desarrollador, mientras tú optimizas límites de seguridad previsibles. Esos objetivos no son enemigos, pero requieren un diseño explícito. No ganas esperando que Docker cambie.
Guía de diagnóstico rápido (primero / segundo / tercero)
Cuando un puerto parece abierto inesperadamente, o el tráfico de contenedores evita tu política prevista, no empieces a reescribir reglas. Comienza con visibilidad. Luego decide quién debe ser el dueño de qué.
Primero: confirma qué está activo (ruleset nft + vista iptables)
- Vuelca el ruleset activo de nft y busca cadenas de Docker y puntos de salto.
- Revisa las reglas iptables tal como se ven a través de la capa de compatibilidad (puede revelar cómo Docker insertó reglas).
Segundo: traza la ruta del paquete para el síntoma específico
- ¿Es entrada hacia un puerto publicado? Eso es DNAT + filter/forward.
- ¿Es egress de contenedor? Eso es masquerade + forward + sysctls.
- ¿Es contenedor a contenedor? Eso es filtrado del bridge y los equivalentes de la cadena
FORWARD.
Tercero: decide el modelo y aplícalo
- Si Docker gestiona iptables/nft: fija la política en
DOCKER-USER, mantén tus reglas nft compatibles, evita hooks en conflicto. - Si tú gestionas todo: deshabilita el manejo iptables de Docker, implementa tus propias reglas nat/filter para bridges y puertos publicados, y acepta la sobrecarga operativa.
La mayoría de los equipos de producción deberían empezar con “Docker gestiona NAT pero nosotros aplicamos la política vía DOCKER-USER” a menos que tengan requisitos estrictos de segmentación de red y el personal para mantenerlo.
Tareas prácticas (comandos, salidas, decisiones)
Estas son tareas de campo. Ejecútalas en el host. Lee la salida. Toma una decisión. Repite hasta que el sistema sea aburrido.
Tarea 1 — Confirma el estado de Docker y nftables
cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-30 09:14:22 UTC; 2h 1min ago
Docs: https://docs.docker.com
Main PID: 1423 (dockerd)
Tasks: 22
Memory: 154.2M
CPU: 1min 12.553s
CGroup: /system.slice/docker.service
└─1423 /usr/bin/dockerd -H fd://
Qué significa: Docker está activo y puede estar inyectando reglas ahora mismo.
Decisión: Asume que el firewall no es “estático”. Procede a inspeccionar reglas activas, no archivos de configuración.
cr0x@server:~$ systemctl status nftables --no-pager
○ nftables.service - nftables
Loaded: loaded (/lib/systemd/system/nftables.service; enabled; preset: enabled)
Active: inactive (dead)
Qué significa: nftables puede no estar cargando tu baseline esperado al arranque, pero el kernel aún puede tener reglas insertadas por otros componentes.
Decisión: Si esperas un firewall baseline, corrige el estado del servicio más tarde. Primero, inspecciona el ruleset en vivo.
Tarea 2 — Volcar el ruleset activo de nftables (la verdad)
cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport 22 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
jump DOCKER-USER
jump DOCKER-FORWARD
}
chain output {
type filter hook output priority filter; policy accept;
}
chain DOCKER-USER {
return
}
chain DOCKER-FORWARD {
iif "docker0" accept
oif "docker0" accept
}
}
Qué significa: Existen cadenas relacionadas con Docker dentro de tu tabla inet filter, y forward salta a ellas. Incluso si no las creaste.
Decisión: Si quieres control de política mientras Docker sigue funcionando, DOCKER-USER es tu punto de aplicación.
Tarea 3 — Revisa la tabla nat de nft para DNAT/MASQUERADE
cr0x@server:~$ sudo nft list table ip nat
table ip nat {
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
iifname != "docker0" tcp dport 8080 dnat to 172.18.0.5:80
}
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
oifname != "docker0" ip saddr 172.18.0.0/16 masquerade
}
}
Qué significa: El puerto 8080 del host está siendo DNATeado a un contenedor, y el egress del contenedor está masqueradeado.
Decisión: Si ves reglas DNAT que no autorizaste, encuentra qué contenedores publicaron puertos y decide si la publicación debe permitirse.
Tarea 4 — Identificar puertos publicados desde la perspectiva de Docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
web-01 nginx:alpine 0.0.0.0:8080->80/tcp
redis-01 redis:7 6379/tcp
Qué significa: web-01 está publicado en todas las interfaces del host en el puerto 8080. Redis no está publicado (solo en el contenedor).
Decisión: Si “0.0.0.0” no es aceptable, limita a 127.0.0.1:8080:80 o a una IP de interfaz específica, o bloquea en DOCKER-USER.
Tarea 5 — Confirma qué backend de iptables está activo
cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,40p'
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
link iptables is /usr/sbin/iptables
/usr/sbin/iptables-nft - priority 20
/usr/sbin/iptables-legacy - priority 10
Qué significa: Los comandos iptables se mapean a reglas nft (backend nft). La programación de iptables por parte de Docker acabará en nftables.
Decisión: No mezcles “iptables-legacy” con reglas nft nativas a menos que disfrutes depurar universos paralelos.
Tarea 6 — Ver las reglas iptables como Docker las ve (nft-backed)
cr0x@server:~$ sudo iptables -S | sed -n '1,80p'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-USER
-N DOCKER-ISOLATION-STAGE-1
-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 DOCKER-USER -j RETURN
Qué significa: Docker insertó su plomería estándar. La presencia de DOCKER-USER es tu oportunidad para aplicar política.
Decisión: Si vas a mantener a Docker gestionando reglas, pon tus drops/accepts en DOCKER-USER (o en la cadena nft equivalente si eres totalmente nativo).
Tarea 7 — Demuestra si el host realmente está escuchando o si es DNAT
cr0x@server:~$ sudo ss -lntp | awk 'NR==1 || $4 ~ /:8080$/'
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=2331,fd=4))
Qué significa: Un proceso (a menudo docker-proxy, según la configuración) está ligado al puerto 8080; en otras configuraciones puede no haber un listener porque se usa DNAT puro.
Decisión: Si esperabas “sin listener en host significa cerrado”, corrige esa asunción. Debes inspeccionar también nat/filter.
Tarea 8 — Inspecciona los sysctls que controlan el reenvío y el filtrado de bridges
cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
Qué significa: El reenvío está habilitado y el tráfico del bridge llega a los hooks de netfilter. Eso es típico para Docker, pero no siempre deseado en entornos muy controlados.
Decisión: Si el tráfico de contenedores nunca debe enrutar entre redes, considera deshabilitar el reenvío globalmente (pero entiende que la red de Docker cambiará) o aislar mediante reglas.
Tarea 9 — Encuentra qué interfaz recibe realmente el tráfico “sorpresa”
cr0x@server:~$ ip -brief addr
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 203.0.113.10/24 2001:db8:10::10/64
docker0 DOWN 172.17.0.1/16
br-2a1d3c4e5f6a UP 172.18.0.1/16
Qué significa: Tu interfaz pública es ens3; existen redes Docker en br-....
Decisión: Para política de entrada, siempre razona desde la interfaz de ingreso (a menudo ens3) a través de nat PREROUTING y luego forward/filter.
Tarea 10 — Confirma qué contenedor posee el destino DNAT
cr0x@server:~$ docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web-01
/web-01 172.18.0.5
Qué significa: La IP destino del DNAT coincide con web-01.
Decisión: Si ese contenedor no debe ser accesible desde Internet, corrige la configuración de publicación y/o bloquéalo en DOCKER-USER.
Tarea 11 — Añade una puerta de política explícita en DOCKER-USER (bloquear entrada excepto aprobados)
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -i ens3 -p tcp --dport 8080 -s 198.51.100.0/24 -j ACCEPT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -i ens3 -p tcp --dport 8080 -j DROP
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN
Qué significa: La entrada al puerto publicado 8080 desde la interfaz pública ahora está restringida a una CIDR permitida; de lo contrario se descarta, antes de las propias reglas accept de Docker.
Decisión: Si esto arregla la discrepancia de política sin romper la red de contenedores, has validado el modelo “Docker gestiona la plomería, nosotros aplicamos la política”.
Tarea 12 — Haz las reglas persistentes (y deja de confiar en ediciones manuales)
cr0x@server:~$ sudo install -d -m 0755 /etc/iptables
cr0x@server:~$ sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
cr0x@server:~$ sudo head -n 20 /etc/iptables/rules.v4
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -i ens3 -p tcp -s 198.51.100.0/24 --dport 8080 -j ACCEPT
-A DOCKER-USER -i ens3 -p tcp --dport 8080 -j DROP
-A DOCKER-USER -j RETURN
COMMIT
Qué significa: Ahora tienes un registro durable de las reglas de política aplicadas. Si lo restauras con una unidad systemd u otro mecanismo es tu elección; el punto es que “funcionó el martes” no es una estrategia de configuración.
Decisión: Si el estándar de tu organización es nftables nativo, traduce esto a sintaxis nft y cárgalo vía el servicio nftables. Si el estándar es iptables-nft, mantenlo consistente y automatiza la restauración al arranque.
Tarea 13 — Si quieres que Docker deje de inyectar reglas por completo (modo de alta ceremonia)
cr0x@server:~$ sudo install -d -m 0755 /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"iptables": false,
"ip-forward": false
}
EOF
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
web-01 0.0.0.0:8080->80/tcp
Qué significa: Docker aún informa puertos publicados, pero ya no instalará las reglas de netfilter para hacerlos accesibles. Algo dejará de funcionar hasta que suministres tus propias reglas NAT/forward/filter y sysctls.
Decisión: Elige este modo solo si estás preparado para implementar y asumir la responsabilidad del NAT y el reenvío correctamente para cada red Docker que crees.
Tarea 14 — Verifica una ruta de paquete específica con contadores (visibilidad nativa nft)
cr0x@server:~$ sudo nft -a list chain inet filter forward
table inet filter {
chain forward { # handle 8
type filter hook forward priority filter; policy drop;
jump DOCKER-USER # handle 21
jump DOCKER-FORWARD # handle 22
}
}
Qué significa: Puedes referenciar handles de reglas, añadir contadores y observar aciertos. Así es como dejas de discutir sobre “debería” y empiezas a medir “es”.
Decisión: Si no puedes explicar qué cadena maneja tu tráfico, no cambies la política todavía—instrumenta primero.
Chiste #2: Si alguna vez te sientes inútil, recuerda que existe una solicitud de cambio de firewall marcada “urgente” sin IP origen y sin puerto.
Dos diseños sensatos (y uno que parece sensato)
Diseño A (recomendado para la mayoría de equipos): Docker gestiona la plomería, tú aplicas política en DOCKER-USER
Dejas que Docker cree DNAT/MASQUERADE y mantenga los contenedores accesibles como fue diseñado. Luego añades reglas explícitas de permitir/denegar en DOCKER-USER basadas en:
- Interfaz de ingreso (pública vs privada)
- Puerto de destino (servicios publicados)
- CIDR de origen (redes administrativas, rangos VPN, redes de socios)
Por qué funciona: la generación de reglas de Docker es compleja y dinámica (contenedores arrancan/detienen, redes aparecen/desaparecen). Tu política es comparativamente estable. Pon la parte estable donde siempre se evaluará temprano.
Qué evitar: esparcir reglas de drop en cadenas aleatorias esperando que ganen a las accept de Docker. Eso no es ingeniería; es vibra.
Diseño B (para entornos estrictos): desactivar la programación de firewall de Docker y gestionar todo en nftables
Si tienes restricciones regulatorias o estás construyendo una plataforma donde Docker es “solo otra carga”, puedes desactivar el comportamiento iptables de Docker. Entonces:
- Habilitas el reenvío selectivamente.
- Creas reglas nft nat para cada subred de bridge de Docker.
- Permites explícitamente el reenvío para puertos publicados y egress deseado.
Es viable, pero es trabajo. Necesitas pruebas, automatización y alguien de guardia que pueda razonar sobre el flujo de paquetes a las 03:00.
Diseño C (parece sensato pero no lo es): mezclar reglas nft nativas con arreglos ad-hoc de iptables
El modo de fallo: “arreglas” un problema con un comando iptables por prisa, luego más tarde “arreglas” otro con sintaxis nft, luego Docker actualiza y reescribe piezas, y ahora el conjunto de reglas es una tarta en capas de arrepentimiento.
Elige un plano de control para la política escrita por humanos: o nft nativo con una configuración de Docker controlada, o iptables-nft con uso de cadenas controlado. La consistencia vence a la astucia.
Tres microhistorias corporativas (anonimizadas, plausibles, técnicamente precisas)
Incidente: la suposición incorrecta (“Lo bloqueamos en INPUT, así que no puede alcanzarse”)
Una empresa SaaS mediana migró un conjunto de herramientas internas a una flota de VMs basadas en Debian. Línea base de seguridad: nftables con política por defecto drop en INPUT, SSH solo desde la VPN corporativa y todo lo demás cerrado. El despliegue parecía limpio.
Luego un desarrollador desplegó una UI de administración en contenedor y la publicó con -p 8443:443 para “probarla rápidamente”. Nadie lo notó porque no había un servicio en escucha en el host en 8443 de la forma habitual, y la cadena INPUT permaneció intacta: drop por defecto, sin allow para 8443. Todos se relajaron.
Una semana después, un escaneo externo marcó 8443 como abierto. El ingeniero de guardia hizo lo habitual: revisó ss -lntp, vio un listener relacionado con docker, se encogió de hombros y añadió un drop en INPUT. El escaneo seguía mostrando abierto. Ahora el humor pasó de “normal” a “estamos siendo encantados”.
Causa raíz: el tráfico estaba siendo DNATeado y reenviado. La cadena INPUT no era la única puerta; la ruta FORWARD más las reglas instaladas por Docker aceptaban efectivamente el tráfico reenviado. La regla de drop del ingeniero estaba en el hook equivocado para la ruta del paquete.
Solución: aplicar política en DOCKER-USER (descartar entrada a puertos publicados excepto desde rangos VPN) y cambiar la publicación del contenedor para ligar solo a la interfaz VPN. Acción postmortem: dejar de tratar INPUT como la única frontera del firewall en hosts con contenedores.
Optimización que salió mal: “Deshabilitar docker-proxy y confiar en nft puro”
Un equipo de plataforma quería ganancias de rendimiento y mejor observabilidad. Deshabilitaron docker-proxy (un ajuste común) y se estandarizaron en nftables. Esperaban menos procesos, menos piezas móviles y una exposición de puertos más nativa al kernel.
En staging todo parecía más rápido y “más nativo del kernel”. Luego producción sufrió un bug peculiar: ciertos puertos publicados eran accesibles desde algunas redes pero no desde otras, y las comprobaciones de salud empezaron a fallar solo para clientes IPv6. Los ingenieros pasaron días discutiendo si era comportamiento del balanceador, límites de conntrack o una actualización de kernel defectuosa.
El verdadero problema fue deriva de política: las reglas nft del equipo asumían que los sockets en escucha representarían la exposición, pero con proxy deshabilitado la exposición era mayormente comportamiento DNAT. Su monitor comprobaba ss por listeners y se perdía reglas que abrían caminos. Su política IPv6 era incompleta: el comportamiento de NAT y filtrado era inconsistente entre tablas ip e inet.
Solución: trata la publicación de puertos como una característica del firewall, no como una característica de proceso. Monitorea deltas del ruleset nftables (o al menos conteos de cadenas y reglas seleccionadas), aplica política vía DOCKER-USER y diseña explícitamente el comportamiento IPv6 (o lo soportas completamente o lo deshabilitas intencionalmente).
La optimización no fue “mala”. Fue complejidad sin dueño. Así es como “mejoras de rendimiento” se convierten en “incidentes de disponibilidad”.
Práctica aburrida pero correcta que salvó el día: volcado de reglas en tickets de incidente
Una gran empresa con un proceso de cambios conservador tenía una regla simple: cada ticket de incidente relacionado con firewall debía incluir adjuntos de nft list ruleset, iptables -S y un listado de puertos de docker ps tomado al momento del impacto. Los ingenieros se quejaban porque parecía burocrático.
Luego una integración con un appliance de un proveedor empezó a fallar intermitentemente. El tráfico desde un rango IP de un socio a veces llegaba a un endpoint en contenedor y a veces caía en un agujero negro. El equipo de la aplicación culpó al socio. El socio culpó a la empresa. Todos estaban a punto de programar una reunión (la forma tradicional de resolver pérdida de paquetes).
El de guardia siguió la regla aburrida y adjuntó los volcados. Un revisor notó que el ruleset activo cambió después de un redeploy de un contenedor: apareció un nuevo bridge definido por el usuario, y Docker insertó reglas de masquerade correspondientes. Las reglas de drop personalizadas de la empresa estaban ligadas a nombres de interfaz que cambiaban con el bridge. La política era correcta en intención pero frágil en implementación.
Porque tenían volcados “antes/después”, no necesitaron conjeturas. Reescribieron la política para que hiciera match por rangos de direcciones y usar DOCKER-USER para restricción de ingreso en lugar de anclar nombres de bridges efímeros. Se arregló el mismo día sin arrastrar a diez personas a una reunión.
Práctica aburrida. Resultado correcto. Ese es el trabajo.
Errores comunes: síntomas → causa raíz → solución
1) “El puerto está abierto aunque INPUT sea drop”
Síntomas: Un escaneo externo muestra un puerto publicado alcanzable; tu cadena nft input no tiene regla allow para él.
Causa raíz: El tráfico se DNATea en PREROUTING y atraviesa FORWARD, no INPUT. Las reglas accept de forward de Docker (o tu forward permisivo) lo permiten.
Solución: Aplica restricciones de ingreso en DOCKER-USER (preferido) o en el hook forward antes de que Docker acepte. Verifica las reglas nat PREROUTING para el puerto.
2) “Bloqueé un contenedor, pero aún llega a Internet”
Síntomas: Añades drops en una cadena input del host; el egress del contenedor aún funciona.
Causa raíz: El egress del contenedor es tráfico reenviado; INPUT es irrelevante. Además, el egress está a menudo masqueradeado, ocultando la IP del contenedor a menos que hagas match por interfaces/subredes.
Solución: Bloquea en la ruta forward basándote en la subred origen (rangos del bridge Docker) o en la IP del contenedor, o haz match por iifname de los bridges de Docker. Prefiere redes controladas por aplicación.
3) “Después de habilitar el servicio nftables, la red de Docker dejó de funcionar”
Síntomas: Los contenedores no pueden alcanzar el exterior; los puertos publicados dejan de responder tras reiniciar.
Causa raíz: El servicio nftables carga un baseline que vacía o sobrescribe cadenas que Docker espera, o establece política forward drop sin proporcionar los puntos de salto que Docker necesita.
Solución: Decide el modelo de propiedad. Si Docker gestiona reglas, no vacíes tablas de las que Docker depende; incorpora los saltos necesarios o mantén la política en DOCKER-USER. Si tú eres el dueño, deshabilita iptables de Docker e implementa reglas nat/forward equivalentes.
4) “Las reglas parecen correctas en nft, pero iptables muestra otra cosa”
Síntomas: El volcado nft no coincide con lo que una herramienta basada en iptables afirma, o viceversa.
Causa raíz: Mezclar iptables legacy con backend nft, o tener ambos activos de forma confusa, o asumir que una herramienta muestra todo.
Solución: Estandariza en iptables-nft si debes usar sintaxis iptables. Evita legacy. Verifica siempre con nft list ruleset porque ese es el estado visible al kernel.
5) “IPv6 se comporta diferente (o elude restricciones misteriosamente)”
Síntomas: Las restricciones IPv4 funcionan; clientes IPv6 aún pueden conectar, o lo contrario.
Causa raíz: Las reglas solo se aplicaron a tablas ip (v4), no a inet; o Docker/host tiene IPv6 habilitado con cadenas y políticas distintas.
Solución: Usa table inet para reglas de filtrado cuando quieras paridad, y decide explícitamente si habilitar IPv6 en Docker. Prueba ambos stacks, no asumas.
6) “Un reinicio cambió todo”
Síntomas: El comportamiento del firewall difiere tras reinicio; puertos se abren/cierran inesperadamente.
Causa raíz: No hay un orden de arranque determinista: nftables carga después de Docker o viceversa, y uno vacía/sobrescribe al otro; o las reglas se añadieron manualmente y nunca se persistieron.
Solución: Haz explícito el orden de arranque con dependencias systemd, persiste reglas correctamente e incluye comprobaciones de verificación en la gestión de configuración.
Listas de verificación / plan paso a paso
Lista A — Quieres que Docker siga funcionando, pero buscas seguridad predecible
- Elige un único plano de control de política: escoge o nftables nativo con cadenas estables, o iptables-nft para inserción de política. No improvises.
- Confirma el backend: asegúrate de que
iptablesapunta aiptables-nfty no al legacy. - Inventario de exposición: lista puertos publicados vía
docker psy correspondelos con reglas nat de nft. - Aplica política de ingreso en DOCKER-USER: permite solo los orígenes que pretendes; descarta el resto.
- Gestiona IPv6 deliberadamente: replica la política usando tablas inet, o deshabilita IPv6 para Docker si tu entorno no puede soportarlo de forma segura.
- Persiste cambios: almacena las fuentes de reglas en gestión de configuración y asegúrate de que se aplican al arranque.
- Verifica con contadores: añade contadores a reglas clave y valida aciertos durante pruebas.
- Escribe una nota para operadores: “Los puertos publicados se controlan desde DOCKER-USER; no abras puertos en INPUT esperando que funcione.”
Lista B — Quieres que Docker deje de inyectar reglas (propiedad total)
- Configura daemon.json de Docker: deshabilita
iptablesy decide sobreip-forward. - Define un plan de direcciones: fija subredes de bridge de Docker para que las reglas no persigan la aleatoriedad.
- Escribe reglas nft nat: MASQUERADE para egress, DNAT para cada puerto publicado que permitas.
- Escribe reglas nft de filtrado: reglas de forward para flujos establecidos, permite solo ingreso necesario a contenedores.
- Prueba comportamiento tras reinicio: reinicia Docker y el host; asegura que las reglas permanezcan consistentes.
- Actualiza runbooks: “Publicar un puerto en Docker no hace nada a menos que se añadan reglas de firewall.”
- Automatiza la verificación: CI o una comprobación en arranque que afirme que las cadenas clave existen y las políticas son correctas.
Lista C — Política mínima que evita sorpresas (una buena línea base inicial)
- Drop por defecto en INPUT del host.
- Drop por defecto en FORWARD a menos que explícitamente permitas el reenvío de Docker.
- ct state established,related accept temprano y explícito.
- Política explícita en
DOCKER-USERpara puertos publicados: permite solo desde CIDR de confianza. - Restringe puertos publicados a interfaces específicas cuando sea posible (ligar a IP VPN, no a 0.0.0.0).
Preguntas frecuentes
1) ¿Por qué Docker toca mi firewall?
Porque los contenedores necesitan NAT y reenvío para ser útiles por defecto, y los puertos publicados requieren reglas DNAT. Docker optimiza “instalar y ejecutar”, no “tu modelo de cumplimiento”. Si quieres otro comportamiento, debes configurarlo.
2) Uso nftables. ¿Por qué veo cadenas de iptables como DOCKER-USER?
En Debian 13, iptables suele usar el backend nft (iptables-nft). Docker programa semántica de iptables, que se convierte en objetos nftables bajo el capó. Sintaxis distinta, mismo conjunto de reglas del kernel.
3) ¿Es DOCKER-USER el lugar correcto para poner mis reglas de seguridad?
Si dejas que Docker gestione la plomería iptables/nft, sí. Docker salta a DOCKER-USER temprano en el reenvío, específicamente para que los operadores puedan aplicar política antes de las reglas accept de Docker. Úsalo.
4) ¿Por qué bloquear en la cadena nft input no detuvo el acceso a un puerto publicado?
Porque el paquete puede no atravesar INPUT. Con DNAT, el kernel puede tratar el tráfico entrante como tráfico reenviado hacia otra interfaz (el bridge Docker), así que FORWARD es la puerta.
5) ¿Debo desactivar la gestión iptables de Docker?
Solo si tienes una razón sólida y la madurez operativa para asumir NAT/reenvío de extremo a extremo. De lo contrario, mantén la plomería de Docker y aplica política en el punto de estrangulamiento previsto.
6) ¿Cómo evito que los desarrolladores publiquen servicios accidentalmente a Internet?
Combina controles: aplica drop por defecto en DOCKER-USER para ingreso desde la interfaz pública, y exige publicar solo en loopback o en una interfaz privada/VPN. Añade comprobaciones en CI para archivos Compose y monitorización en tiempo de ejecución de puertos publicados.
7) ¿Qué pasa con Docker rootless?
Rootless cambia el modelo de red y a menudo reduce la manipulación directa de netfilter, pero introduce otros componentes (slirp4netns, namespaces de usuario) y diferentes compromisos de rendimiento/funcionalidad. Puede ayudar a que “Docker no reescriba reglas de firewall”, pero no es un reemplazo universal para cargas en producción.
8) ¿Por qué desaparecen las reglas tras un reinicio?
Normalmente por orden de arranque y persistencia. Docker puede añadir reglas al iniciar; el servicio nftables puede luego vaciarlas o reemplazarlas; o añadiste reglas manualmente y nunca las guardaste. Soluciona haciendo explícito el modelo de propiedad y asegurando que el servicio correcto cargue las reglas correctas en el momento correcto.
9) ¿Puedo gestionar todo en un único ruleset nftables y seguir usando Docker con normalidad?
Sí, pero debes alinear cómo Docker espera que existan cadenas y hooks, o deshabilitar la gestión de reglas de Docker y reproducir el comportamiento NAT/forward requerido tú mismo. La meta de “un único ruleset” está bien; la creencia de que “las sorpresas se detendrán automáticamente” no lo está.
Conclusión: próximos pasos que puedes hacer hoy
Si Docker sorprendió a tu firewall nftables en Debian 13, la solución no requiere heroísmos. Requiere claridad. Decide quién posee la programación de netfilter, luego aplica política en el hook correcto con la herramienta adecuada.
- Vuelca el ruleset en vivo (
nft list ruleset) e identifica cadenas y puntos de salto de Docker. - Inventaría puertos publicados (
docker ps) y córrelos con reglas nat DNAT. - Implementa una puerta de política en
DOCKER-USERpara restringir el acceso entrante a puertos publicados. - Hazlo persistente y prueba reinicios para que “funcionó una vez” se convierta en “funciona siempre”.
- Documenta la ruta del paquete para tu equipo: INPUT no es la única puerta.
Tu firewall debería ser aburrido. Docker no lo hará aburrido por ti. Ese es tu trabajo.