Siempre empieza igual: “los contenedores no pueden acceder a internet”, “el puerto 443 dejó de publicarse” o el favorito del público, “ayer funcionaba”. Compruebas Docker: está en ejecución. Compruebas la aplicación: está sana. Luego miras el cortafuegos y descubres que no estás depurando “redes”. Estás depurando dos motores de cortafuegos que ambos creen que mandan.
En Linux moderno, iptables puede ser una capa de compatibilidad sobre nftables. O puede ser el binario legacy. Docker sigue hablando iptables. Tu distro puede preferir nftables. Añade un gestor de cortafuegos que recarga reglas cada vez que estornuda, y tienes una guerra de producción conducida enteramente en tablas del kernel.
Qué ocurre realmente cuando Docker “rompe” la red
El modelo de red por defecto de Docker en un solo host es engañosamente simple:
- Crear un bridge de Linux (habitualmente
docker0). - Adjuntar pares veth de los contenedores a ese bridge.
- Hacer NAT (MASQUERADE) para que los contenedores puedan alcanzar el exterior.
- Añadir reglas de filtrado para que el reenvío funcione y para que los puertos publicados lleguen al contenedor correcto.
Cada uno de esos pasos toca netfilter. Lo que duele es que “iptables” no es solo una herramienta; también es una interfaz a las tablas de reglas del kernel. Y en los últimos años, la herramienta en espacio de usuario ha estado jugando a las sillas musicales:
- iptables-legacy: habla con la interfaz histórica xtables.
- iptables-nft: el comando iptables que escribe reglas en nftables (a través de una capa de compatibilidad).
- nft: la herramienta nativa de nftables y su lenguaje.
Si Docker está escribiendo reglas con un backend pero tu sistema está inspeccionando o gestionando el otro backend, “no verás nada”, creerás que no hay nada configurado y luego “arreglarás” el problema empeorándolo con confianza.
También hay tres capas de política que rutinariamente chocan:
- Configuraciones del kernel como
net.ipv4.ip_forwardy los sysctls de bridge netfilter. - Gestores de cortafuegos (firewalld, ufw, unidades systemd personalizadas, gestión de configuración) que establecen una política por defecto y recargan reglas.
- Gestión de reglas de Docker, que asume que puede insertar cadenas como
DOCKERyDOCKER-USERy luego saltar a ellas.
Los modos de fallo son consistentes:
- Las reglas NAT existen en un conjunto de reglas, pero los paquetes son evaluados por otro.
- El reenvío está bloqueado porque la política por defecto es DROP, o porque una recarga del cortafuegos olvidó los saltos de Docker.
- Los puertos publicados no funcionan porque faltan reglas DNAT o la cadena
DOCKERno está referenciada. - El tráfico entre contenedores muere porque el filtrado de bridge está habilitado y no lo permitiste.
Elige un bando. Hazlo estable. Deja de permitir que tres herramientas garabateen en el mismo cuaderno.
Un chiste corto, como prometí: NAT es como la política de oficina: todo funciona hasta que alguien “simplifica” las reglas y de repente nadie puede hablar con nadie.
Guía rápida de diagnóstico (primeras/segundas/terceras comprobaciones)
Primero: confirma el backend del cortafuegos y dónde se escriben las reglas
- ¿Está
iptablesusando nft o legacy? - ¿
nft list rulesetmuestra cadenas/reglas de Docker? - ¿Estás inspeccionando el mismo backend que Docker está programando?
Segundo: confirma que exista reenvío y NAT para el bridge de Docker
- ¿
net.ipv4.ip_forward=1? - ¿Regla MASQUERADE para la subred de
docker0? - ¿La cadena FORWARD permite
docker0→ externo y el tráfico de retorno?
Tercero: localiza quién está sobrescribiendo reglas después de que Docker arranque
- ¿Se recarga firewalld? ¿ufw se habilita? ¿scripts de hardening personalizados?
- Orden systemd: ¿arranca Docker antes/después de tu servicio de cortafuegos?
- ¿Desaparecen las reglas tras una recarga del cortafuegos?
Cuando estás bajo presión
Si esto es producción y el sitio está caído, la ruta más rápida y segura suele ser:
- Asegurarte de que Docker y tu host usan el mismo backend (a menudo seleccionando iptables-legacy o alineando todo a nftables).
- Restaurar reenvío + NAT y confirmar con contadores de paquetes.
- Evitar futuros borrados de reglas corrigiendo el orden de servicios y usando
DOCKER-USERpara tu política.
Luego vuelves y lo dejas correcto, no solo “funcionando ahora”.
Hechos interesantes y breve historia (para entender la rareza)
- nftables llegó en Linux 3.13 (2014) como sucesor de iptables, ofreciendo un conjunto de reglas más flexible y mejor rendimiento con grandes cantidades de reglas.
- iptables-nft es una capa de compatibilidad, no “iptables con salida diferente”. Traduce reglas de iptables a objetos nftables, y la traducción tiene casos límite.
- Algunas distribuciones cambiaron iptables a nft por defecto (vía alternatives), lo que cambió silenciosamente lo que significa
iptables -Sen la misma línea de comandos. - El modelo de red de Docker es anterior a nftables, y sus suposiciones operativas se construyeron alrededor de cadenas iptables que puede insertar y gestionar incrementalmente.
- nftables soporta actualizaciones atómicas del ruleset, lo cual es bueno para la corrección; también significa que una herramienta puede reemplazar todo el ruleset de un golpe y eliminar accidentalmente las cadenas de Docker.
- firewalld evolucionó hacia nftables, pero muchos entornos todavía ejecutan herramientas mixtas donde Docker usa iptables y firewalld usa nftables directamente.
- La cadena DOCKER-USER existe por una razón: Docker necesitaba un punto de inserción estable para la política de usuario para que no fuera sobrescrita por las propias actualizaciones de Docker.
- Bridge netfilter es una fuente común de errores: los paquetes que atraviesan bridges de Linux pueden pasarse a iptables/nftables dependiendo de sysctls, lo que cambia la ruta que deben coincidir tus reglas.
- Los fallos de redes en contenedores suelen ser “fallos de política” no “fallos de enrutamiento”: las rutas parecen correctas, ARP funciona, pero netfilter silenciosamente descarta o no hace NAT.
Tareas prácticas: comandos, salidas y la decisión que tomas
Estas son las comprobaciones que ejecuto cuando el gráfico está en rojo y Slack suena fuerte. Cada tarea incluye: un comando, salida realista, qué significa y qué decisión tomas.
Task 1: Identify which iptables backend you’re using
cr0x@server:~$ sudo iptables --version
iptables v1.8.9 (nf_tables)
Qué significa: Tu comando iptables escribe en nftables (iptables-nft). Si Docker usa el mismo binario, sus reglas viven en nftables, no en xtables legacy.
Decisión: Debes inspeccionar con nft o con iptables de forma consistente. Si otro componente usa iptables-legacy, tienes split-brain.
Task 2: Check alternatives (Debian/Ubuntu) for iptables and friends
cr0x@server:~$ sudo update-alternatives --display iptables
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-legacy - priority 10
/usr/sbin/iptables-nft - priority 20
Qué significa: La preferencia del sistema es iptables con backend nft. Tu herramienta y expectativas deben coincidir con eso.
Decisión: Si encuentras que Docker o tu cortafuegos usa legacy, alínialos (mueve todo a nft o cambia deliberadamente a legacy por simplicidad de la era Docker).
Task 3: Confirm Docker’s bridge and subnets
cr0x@server:~$ docker network inspect bridge --format '{{json .IPAM.Config}}'
[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
Qué significa: La subred del bridge por defecto es 172.17.0.0/16. Las reglas de NAT y reenvío deben referenciar ese CIDR o la interfaz.
Decisión: Si usas subredes personalizadas, deben verse aquí y verificar que el cortafuegos coincida. Subredes desajustadas = NAT que nunca se dispara.
Task 4: Verify kernel forwarding is enabled
cr0x@server:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
Qué significa: El kernel no reenviará paquetes entre interfaces. Los contenedores pueden hablar con el host, pero no más allá.
Decisión: Habilita el reenvío permanentemente vía sysctl. Los cambios temporales son solo para respuesta a incidentes.
cr0x@server:~$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
Task 5: Check bridge netfilter sysctls (common in hardened builds)
cr0x@server:~$ sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables 2>/dev/null
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
Qué significa: El tráfico en bridges se pasa a iptables/nftables para filtrado. Eso puede estar bien, pero significa que las políticas en FORWARD importan mucho.
Decisión: Si no querías filtrar el tráfico intra-bridge, considera poner estos valores a 0, o escribe reglas correctas. No adivines.
Task 6: See whether Docker created its expected chains (iptables view)
cr0x@server:~$ sudo iptables -S | sed -n '1,40p'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-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 FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
Qué significa: Docker insertó cadenas, pero la política por defecto de FORWARD es DROP. Docker intenta abrir huecos; los gestores de cortafuegos a veces reinician políticas y rompen esto.
Decisión: Si el tráfico sigue fallando, inspecciona contadores y confirma que estos saltos sigan existiendo después de recargas del cortafuegos. Si no, el gestor está sobrescribiéndolos.
Task 7: Verify NAT MASQUERADE exists for container egress
cr0x@server:~$ sudo iptables -t nat -S | sed -n '1,80p'
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
Qué significa: MASQUERADE está presente. Si los contenedores aún no alcanzan internet, o bien el reenvío está bloqueado, el DNS está roto, o los paquetes usan una interfaz distinta a la que crees.
Decisión: Si falta esta línea MASQUERADE, arregla primero el conflicto de backend de reglas. Reañadir la regla manualmente es un parche; Docker te peleará después.
Task 8: Inspect the nftables ruleset directly (truth serum)
cr0x@server:~$ sudo nft list ruleset | sed -n '1,80p'
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
}
chain forward {
type filter hook forward priority 0; policy drop;
jump DOCKER-USER
jump DOCKER-ISOLATION-STAGE-1
}
chain output {
type filter hook output priority 0; policy accept;
}
chain DOCKER-USER {
return
}
}
table ip nat {
chain PREROUTING {
type nat hook prerouting priority -100; policy accept;
}
chain POSTROUTING {
type nat hook postrouting priority 100; policy accept;
ip saddr 172.17.0.0/16 oifname != "docker0" masquerade
}
}
Qué significa: Existen reglas relevantes de Docker en nftables. Si iptables -S no muestra nada pero nft sí, probablemente estás usando iptables-legacy al inspeccionar.
Decisión: Estandariza en una herramienta de inspección y gestión. En incidentes, la forma más rápida de perder una hora es mirar el conjunto de reglas equivocado.
Task 9: Prove whether firewall reload wipes Docker rules
cr0x@server:~$ sudo systemctl reload firewalld
cr0x@server:~$ sudo iptables -t nat -S | grep -E 'MASQUERADE|DOCKER' | head
Ejemplo de salida (mal signo):
cr0x@server:~$ sudo iptables -t nat -S | grep -E 'MASQUERADE|DOCKER' | head
Qué significa: Tras la recarga, las reglas nat de Docker desaparecieron (o estás mirando el backend equivocado). Los contenedores perderán egress y los puertos publicados.
Decisión: Arregla la integración: configura el manejo de Docker en firewalld, asegura que Docker arranque después del cortafuegos y vuelva a añadir reglas, o deja de usar un gestor que reemplaza reglasets sin cuidado.
Task 10: Check DOCKER-USER chain for your policy insertion point
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN
Qué significa: Docker creó la cadena pero no la estás usando. Está bien, pero es también donde deberías poner reglas del tipo “bloquear esta CIDR de contenedores para esa red”.
Decisión: Coloca la política corporativa en DOCKER-USER, no editando la cadena DOCKER de Docker. Docker trata sus propias cadenas como un bloc de notas temporal.
Task 11: Confirm port publishing creates DNAT rules
cr0x@server:~$ docker run -d --name webtest -p 8080:80 nginx:alpine
Unable to find image 'nginx:alpine' locally
nginx:alpine: Pulling from library/nginx
Status: Downloaded newer image for nginx:alpine
3c1c0f4c8b2a3d0a5a1f2d7b3f4d9c2d12e4f0f3c2c7c9bb8a1a3c0f8ad9f1b2
cr0x@server:~$ sudo iptables -t nat -S DOCKER | grep 8080
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Qué significa: Docker publicó el puerto 8080 creando una regla DNAT. Si la aplicación sigue inaccesible, o el filtrado FORWARD lo bloquea, o el servicio no está escuchando en el contenedor.
Decisión: Si no aparece DNAT, Docker está fallando al programar iptables (a menudo por capacidades faltantes, flags del daemon o desajuste de backend).
Task 12: Validate connectivity from inside a container (not from the host)
cr0x@server:~$ docker exec -it webtest sh -c 'ip route; wget -qO- --timeout=3 https://example.com | head'
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
Qué significa: La ruta es correcta. Si wget se queda colgado o falla, sospecha de NAT/reenvío/cortafuegos o DNS. Si funciona, tu problema probablemente sea la publicación de puertos entrantes, no el egress saliente.
Decisión: Divide el problema: saliente vs entrante. Fallan por razones diferentes y requieren reglas distintas.
Task 13: Watch packet counters to see if rules match at all
cr0x@server:~$ sudo iptables -t nat -L POSTROUTING -v -n | sed -n '1,10p'
Chain POSTROUTING (policy ACCEPT 120 packets, 7200 bytes)
pkts bytes target prot opt in out source destination
38 2280 MASQUERADE all -- * eth0 172.17.0.0/16 0.0.0.0/0
Qué significa: Contadores incrementándose = la regla NAT está coincidiendo. Si los contadores son cero mientras los contenedores intentan egress, el tráfico puede no estar siendo reenviado, puede salir por otra interfaz o puede estar bloqueado antes.
Decisión: Usa los contadores para evitar cambios de reglas por culto. Si los contadores no se mueven, tu “arreglo” no está en el lugar correcto.
Task 14: Find who last touched netfilter rules (journal clues)
cr0x@server:~$ sudo journalctl -u docker -u firewalld -u ufw --since "2 hours ago" | tail -n 25
Jan 02 09:14:22 server dockerd[1289]: time="2026-01-02T09:14:22.902311" level=info msg="Loading containers: done."
Jan 02 09:14:23 server dockerd[1289]: time="2026-01-02T09:14:23.110022" level=info msg="Default bridge (docker0) is assigned with an IP address 172.17.0.1/16. Daemon option --bip can be used to set a preferred IP address"
Jan 02 09:41:07 server firewalld[1022]: Reloaded firewall rules
Jan 02 09:41:07 server dockerd[1289]: time="2026-01-02T09:41:07.514010" level=warning msg="Failed to program NAT chain: iptables: No chain/target/match by that name."
Qué significa: Se recargó firewalld y luego Docker falló al programar NAT. Esa es la pistola humeante: las tablas de reglas fueron reemplazadas y las referencias que Docker esperaba se rompieron.
Decisión: Corrige el orden de servicios, la configuración del gestor de cortafuegos o reduce la propiedad de reglas (ver sección de estrategia). No sigas reiniciando Docker como “solución”.
Elige una estrategia: iptables legacy, nftables primero, o “Docker no toca mi cortafuegos”
La mayoría de las interrupciones aquí provienen de la indecisión. Los equipos ejecutan por accidente un híbrido: Docker asume iptables, la distro asume nftables y el gestor de cortafuegos asume que posee todo el ruleset. Elige un modelo e implémentalo deliberadamente.
Strategy A: Standardize on iptables-legacy (the “boring and predictable” route)
Si necesitas máxima compatibilidad con herramientas antiguas o estás desenredando años de scripts, iptables-legacy puede ser la ruta menos mala. Reduce la rareza de traducción y se alinea con las suposiciones originales de Docker.
Cuándo elegirlo:
- Distribuciones antiguas o combinaciones kernel/userspace donde las herramientas nft son inconsistentes.
- Mucha memoria operativa sobre la salida de iptables y scripts.
- Poca apetencia para migrar la política del cortafuegos ahora mismo.
Costo: Estás apostando contra la dirección de la distro. Puede estar bien, pero asume la decisión.
Boceto de implementación (Debian/Ubuntu):
cr0x@server:~$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives: using /usr/sbin/iptables-legacy to provide /usr/sbin/iptables (iptables) in manual mode
Punto de decisión: Tras el cambio, reinicia Docker y confirma que las reglas aparecen con iptables -S. Si las reglas aún no aparecen, algo más está mal (permisos, flags del daemon, gestor de cortafuegos que borra).
Strategy B: Go nftables-first (the “modern kernel, modern rules” route)
Hacia donde va Linux. También es donde la gente sufre cuando migra a medias. Si eliges nftables-first, necesitas disciplina sobre la propiedad de reglas e inspección.
Qué significa “nftables-first”:
- Usar
iptablessolo como interfaz de compatibilidad si es necesario, pero inspeccionar connft. - Asegurar que tu gestor de cortafuegos use nftables y no borre las cadenas gestionadas por Docker, o que se integre explícitamente con Docker.
- Persistir reglas nftables correctamente y evitar herramientas que “vacían y reemplazan todo” sin coordinación.
Punto operacional importante: Docker puede seguir emitiendo comandos iptables, pero si esos usan iptables-nft, está bien. La clave es no mezclar nft y legacy y esperar coherencia.
Strategy C: Tell Docker to stop managing iptables (for people who truly control the edge)
Docker tiene una bandera del daemon: --iptables=false. Es tentador. También es una herramienta afilada que corta en silencio.
Cuándo tiene sentido:
- Tienes un equipo de red dedicado que gestiona todo NAT/filtrado con nftables.
- Ejecutas redes de contenedores fijas y los puertos publicados se definen explícitamente en reglas de cortafuegos.
- Aceptas que Docker no “simplemente funcionará” para desarrolladores.
Cuándo es una trampa:
- Dependes de publicación de puertos dinámica o stacks efímeros de compose.
- Varios equipos despliegan contenedores arbitrarios en el mismo host.
Guía de opinión: Si no estás preparado para escribir y mantener lo equivalente a las reglas NAT y de reenvío de Docker tú mismo, no desactives iptables de Docker. El kernel no premiará tu optimismo.
Segundo chiste corto: Desactivar iptables de Docker es como quitar el volante porque “prefieres sentir la carretera”. Definitivamente sentirás la carretera.
The one chain you should respect: DOCKER-USER
Docker te da un lugar soportado para aplicar política: DOCKER-USER. Se evalúa antes que las propias reglas de Docker para tráfico reenviado. Pon tu política de permitir/denegar allí. Mantenla pequeña. Legible. Auditada.
Ejemplo de política: bloquear que los contenedores lleguen a redes RFC1918 excepto una subred de servicios necesaria. (Ajusta según tu entorno.)
cr0x@server:~$ sudo iptables -I DOCKER-USER 1 -d 10.0.0.0/8 -j REJECT
cr0x@server:~$ sudo iptables -I DOCKER-USER 2 -d 172.16.0.0/12 -j REJECT
cr0x@server:~$ sudo iptables -I DOCKER-USER 3 -d 192.168.0.0/16 -j REJECT
cr0x@server:~$ sudo iptables -A DOCKER-USER -j RETURN
Qué significa: Los contenedores aún pueden alcanzar internet (NAT), pero no pueden moverse lateralmente por tu red corporativa. Usas el hook soportado por Docker en lugar de pelear con sus cadenas.
Decisión: Si necesitas política, usa DOCKER-USER. Si necesitas política compleja, considera mover la red de contenedores a algo con políticas de red explícitas (y acepta la sobrecarga operativa).
A single quote (paraphrased idea) to keep you honest
Richard Cook (idea parafraseada): “El éxito oculta la complejidad; el fracaso la revela.”
Cuando la red de Docker funciona, nadie pregunta quién posee el cortafuegos. Cuando falla, aprendes exactamente cuántas herramientas estaban escribiendo reglas a tus espaldas.
Tres micro-historias corporativas desde el frente
Mini-story 1: The incident caused by a wrong assumption
El equipo tenía una rutina: desplegar una nueva imagen de contenedor, vigilar el health check y seguir. Un martes desplegaron un parche menor a un servicio de pago que publicó un puerto en el host. El servicio quedó sano, pero el tráfico de clientes cayó en picado. Desde la perspectiva de la app, nada fallaba. Desde la perspectiva del load balancer, el backend dejó de aceptar conexiones.
El primer interviniente hizo lo que hace todo ingeniero cansado: comprobó docker ps, confirmó -p 443:8443 y luego miró iptables. Sin reglas DNAT. Sin referencias a la cadena DOCKER. “Docker no programó iptables”, dijeron, y reiniciaron Docker. Volvió a funcionar un minuto. Luego falló de nuevo tras una recarga del cortafuegos. Ahí la incidencia se volvió interesante.
La suposición errónea fue sutil: asumieron que el sistema usaba iptables legacy porque iptables -S no mostraba nada útil. En realidad, el host había cambiado a nftables meses antes durante una actualización del SO. Su runbook seguía usando herramientas iptables-legacy, y su paso de verificación inspeccionaba el backend equivocado. Las reglas existían, solo que no donde estaban mirando.
Mientras tanto, un job de hardening de seguridad corría cada 15 minutos y recargaba la política del cortafuegos usando nftables, reemplazando todo el ruleset atómicamente. Las reglas nft respaldadas por iptables de Docker no se preservaban porque el job de hardening no las incluía. Cada cuarto de hora, el puerto publicado desaparecía. Fue un metrónomo perfecto para tiempo de inactividad.
La solución fue aburrida: dejar de reemplazar todo el ruleset de nft, integrar las cadenas requeridas por Docker y fijar la herramienta de inspección. La lección perduró porque dolió: un “simple redeploy” se convirtió en una lección de backend de cortafuegos entregada en producción.
Mini-story 2: The optimization that backfired
Otra compañía buscaba mejorar el tiempo de arranque. Alguien notó que el inicio de Docker a veces tardaba en hosts ocupados. Perfilando, vieron tiempo manipilando reglas iptables. La optimización propuesta: desactivar la gestión de iptables de Docker y dejar que el “cortafuegos real” lo maneje. Arranque más rápido, menos partes móviles, mejor postura de seguridad. En una presentación, parecía madurez.
Lo desplegaron en staging donde las apps tenían puertos estables y redes fijas. Todo pasó. Luego llegó a producción, donde equipos usaban Compose y servicios temporales que publicaban puertos para depuración. Esos puertos empezaron a fallar de la manera más molesta: los contenedores corrían, registraban “listening” y curl local desde el host funcionaba vía IP del contenedor. Pero los puertos publicados en el host estaban muertos porque no se crearon reglas DNAT. El síntoma operativo fue “puertos aleatorios no funcionan”, excelente para una semana de señalar dedos.
Para parchear, añadieron reglas nft estáticas para “los servicios importantes”. Eso ayudó hasta que el siguiente equipo desplegó algo nuevo. Las reglas del cortafuegos se convirtieron en un registro manual de puertos que se desviaba de la realidad. El supuesto ahorro se convirtió en un impuesto de on-call: cada nuevo servicio requería un PR de cortafuegos. Cada incidente necesitaba a alguien que entendiera nftables y redes Docker. La ganancia en tiempo de arranque se pagó con intereses compuestos.
Terminaron volviendo a habilitar la gestión iptables de Docker y movieron la política a DOCKER-USER. El tiempo de arranque empeoró un poco. Las incidencias bajaron drásticamente. Moral: las “victorias” de rendimiento que eliminan automatización a menudo solo trasladan el trabajo a humanos, y los humanos son notoriamente no deterministas.
Mini-story 3: The boring but correct practice that saved the day
Un tercer equipo operaba en un entorno regulado. Trataban el estado del cortafuegos como código: configuración versionada, propiedad explícita e tests de integración que validaban que las cadenas necesarias existieran tras recargas. Nada heroico, solo disciplina. Era el tipo de práctica que se siente lenta hasta que no lo es.
Durante un parche rutinario del SO, la distribución cambió la alternativa por defecto de iptables de legacy a nft. En el primer reboot, la mitad de los contenedores perdió conectividad saliente. Parecía un típico evento “Docker falló tras parche”, el que arruina fines de semana.
Pero su monitorización tenía una comprobación dirigida: verificar que MASQUERADE para la subred de contenedores existiera y que los contadores de paquetes incrementaran bajo tráfico sintético. La alerta saltó en minutos tras el reboot. El on-call siguió un runbook que empezaba por “verificar backend” y “verificar contadores NAT”, no por “reiniciar Docker hasta que se porte bien”.
Encontraron el desajuste rápido: su gestor de cortafuegos cargaba reglas legacy mientras el binario iptables del sistema apuntaba ahora a nftables. Dos universos paralelos. La solución fue igualmente poco glamorosa: fijar alternatives para iptables en la gestión de configuración y añadir una verificación post-reload que chequease el ruleset activo con nft list ruleset.
No pasó nada mágico. Simplemente tuvieron menos suposiciones. Así es como se ve lo “aburrido” cuando funciona.
Errores comunes: síntoma → causa raíz → solución
1) Containers have no internet, but DNS resolves
Síntoma: dig funciona; curl se queda colgado o caduca desde contenedores.
Causa raíz: Falta la regla MASQUERADE o el reenvío está bloqueado (política FORWARD DROP, reglas ACCEPT faltantes).
Solución: Confirma NAT en el backend correcto; asegúrate de net.ipv4.ip_forward=1; garantiza que las reglas FORWARD permitan egress de docker0 y el retorno RELATED,ESTABLISHED.
2) Published ports stopped working after a firewall reload
Síntoma: docker run -p funciona hasta que firewalld/ufw recargan; luego entrantes fallan.
Causa raíz: El gestor de cortafuegos reemplaza el ruleset y elimina las cadenas/saltos DNAT de Docker.
Solución: Configura el gestor para preservar las cadenas de Docker o ordena los servicios para que Docker reprograme reglas tras la recarga. Prefiere la política en DOCKER-USER.
3) iptables shows no Docker rules, but containers are running
Síntoma: iptables -S parece vacío; asumes que Docker no puso reglas.
Causa raíz: Estás mirando iptables-legacy mientras Docker escribe vía iptables-nft (o al revés).
Solución: Revisa iptables --version y alternatives. Inspecciona con nft list ruleset si está respaldado por nft.
4) Inter-container traffic on the same bridge is blocked
Síntoma: Los contenedores alcanzan internet pero no se alcanzan entre sí por IP.
Causa raíz: Bridge netfilter habilitado con reglas FORWARD restrictivas, o cadenas de aislamiento de Docker más reglas personalizadas que bloquean intra-bridge.
Solución: Permite -i docker0 -o docker0 en el camino de forward (o la regla nft equivalente). Revisa los sysctls bridge-nf-call-iptables.
5) Docker fails to start with “Failed to program NAT chain”
Síntoma: Los logs del daemon de Docker muestran errores sobre cadenas/targets faltantes.
Causa raíz: Otra herramienta vació tablas mientras Docker configuraba, o tienes módulos/backend de iptables inconsistentes.
Solución: Corrige el orden de servicios; deja de vaciar tablas detrás de Docker; alinea el backend de iptables; reinicia Docker tras asegurar un estado estable del cortafuegos.
6) Only some hosts in a fleet have the problem
Síntoma: “En host A funciona, en host B falla” tras el mismo despliegue.
Causa raíz: Alternativas iptables diferentes, gestores de cortafuegos distintos o sysctls del kernel distintos por baselines de hardening.
Solución: Estandariza: fija alternatives, aplica sysctls, y verifica con checks automatizados que validen que existen reglas NAT/reenvío en el backend activo.
7) IPv6 works but IPv4 doesn’t (or the opposite)
Síntoma: Los contenedores alcanzan destinos IPv6 pero no IPv4, o viceversa.
Causa raíz: Rutas de reglas separadas: ip6tables vs iptables (o tablas inet en nft). Un lado quedó configurado y el otro no.
Solución: Inspecciona ambas familias. En nft, prefiere table inet para reglas de filter cuando sea apropiado, y asegúrate de que NAT exista para la familia de direcciones que te importe.
Listas de verificación / plan paso a paso
Checklist A: Stabilize a broken host during an incident (15 minutes, no heroics)
- Confirma backend:
iptables --version, luego verifica connft list rulesetsi está respaldado por nft. - Confirma reenvío:
sysctl net.ipv4.ip_forwardy ajústalo a 1 si hace falta. - Confirma NAT: busca MASQUERADE para la subred Docker en el backend activo.
- Confirma ruta FORWARD: asegura que
DOCKER-USERy las reglas ACCEPT de Docker existen y son referenciadas. - Comprueba si una recarga del cortafuegos borra reglas. Si es así, detén la hemorragia: evita recargas temporalmente o reinicia Docker tras la recarga como solución a corto plazo.
- Usa contadores para confirmar que paquetes alcanzan NAT y reglas de forward.
Checklist B: Make the fix permanent (the part everyone skips and then regrets)
- Elige el propietario de la política: o Docker gestiona reglas iptables (por defecto), o tu cortafuegos lo hace y Docker está limitado. No hagas “ambos”.
- Fija backend de iptables de forma consistente: usa alternatives para asegurar uniformidad en la flota.
- Bloquea el orden de servicios: Docker debe arrancar después de que el cortafuegos esté listo, y las recargas no deben borrar las cadenas de Docker.
- Pon la política personalizada en DOCKER-USER: ese es el hook soportado y estable.
- Persiste sysctls: reenvío y sysctls de bridge deben quedar en
/etc/sysctl.d/. - Añade validación: checks post-boot y post-reload que verifiquen MASQUERADE y saltos de cadenas en el backend activo.
Checklist C: A clean migration from legacy iptables to nftables (without surprise downtime)
- Inventaría las reglas iptables actuales e identifica cuáles son gestionadas por Docker vs política del sitio.
- Mueve la política del sitio a
DOCKER-USERdonde sea posible. - Cambia las herramientas de inspección en runbooks de la salida de iptables a nft (o asegúrate que iptables-nft se usa consistentemente).
- Prueba el comportamiento de recarga del cortafuegos en staging: confirma que las cadenas de Docker persisten y que los puertos publicados sobreviven a la recarga.
- Despliega a un pequeño canario de hosts; valida con checks sintéticos de egress/entrantes de contenedores.
- Solo entonces cambia la flota completa.
Preguntas frecuentes
1) Why does Docker still use iptables in 2026?
Porque la interfaz del kernel para filtrado/NAT es netfilter, y iptables ha sido la interfaz práctica durante años. El modelo de Docker se basa en insertar cadenas y reglas de forma incremental. nftables es más reciente y mejor en muchos aspectos, pero “más nuevo” no significa que todos los componentes del ecosistema ya se hayan migrado.
2) What’s the difference between iptables-nft and nftables?
iptables-nft es el comando iptables que escribe reglas en nftables usando una capa de traducción. nftables nativo usa nft y su propio lenguaje y objetos. El detalle operativo importante: las herramientas que gestionan nftables de forma nativa pueden reemplazar rulesets atómicamente y eliminar accidentalmente reglas creadas vía iptables-nft a menos que las preserven intencionalmente.
3) How do I know if I’m looking at the “right” rules?
Empieza con iptables --version. Si muestra (nf_tables), entonces nft list ruleset es la vista autorizada. Si ves (legacy), la salida de iptables refleja las reglas activas. No confíes en la memoria muscular; verifica.
4) Should I use firewalld with Docker?
Puedes, pero debes configurarlo intencionalmente. El modo de fallo principal es que firewalld recargue y borre las reglas de Docker o cambie el comportamiento por defecto de reenvío. Si usas firewalld, prueba el comportamiento de recarga, confirma que las cadenas de Docker persisten y evita políticas que vacíen/reemplacen todo el ruleset sin acomodar a Docker.
5) Where should I put “block containers from reaching X” rules?
Usa la cadena DOCKER-USER. Docker la espera y no la sobrescribirá. No edites las propias cadenas de Docker como almacén de política a largo plazo.
6) Is it safe to restart Docker to “fix networking”?
Es una medida táctica, no una solución. Reiniciar Docker puede reprogramar reglas temporalmente, pero si un gestor de cortafuegos las borra otra vez, volverás aquí—solo con más interrupción. Usa reinicios para restaurar servicio y luego arregla la propiedad y el orden.
7) What about rootless Docker?
Rootless cambia el modelo de red; a menudo usa redes en espacio de usuario (como slirp4netns) en lugar de programar iptables del host. Eso puede evitar la guerra de cortafuegos, pero trae distintos trade-offs de rendimiento y funcionalidad, especialmente para puertos publicados y requisitos de red de bajo nivel.
8) Why does the FORWARD policy being DROP matter if INPUT is ACCEPT?
El tráfico de contenedores que se reenvía por el host pasa por la cadena FORWARD, no por INPUT. INPUT es para tráfico destinado al propio host. Si FORWARD es DROP y no tienes las excepciones correctas, los contenedores quedarán atrapados en su bridge como en una prisión muy educada.
9) Can I run nftables for host security and still let Docker manage NAT?
Sí, si lo haces coherentemente. Deja que Docker programe reglas vía iptables-nft en nftables, y asegúrate de que tu gestión de nftables no borre esas reglas. Pon tu política personalizada en DOCKER-USER (vía iptables) o crea reglas nativas nft que se integren sin vaciar las tablas de Docker.
10) My distro update flipped the backend and now everything is weird. What’s the safest response?
Elige un backend y aplícalo a toda la flota. Durante la estabilización, muchos equipos eligen iptables-legacy porque coincide con suposiciones anteriores. A largo plazo, migrar a nftables-first está bien—pero hazlo con tests que cubran recargas, NAT, reenvío y puertos publicados.
Conclusión: los siguientes pasos que realmente funcionan
Los fallos de red de Docker alrededor de iptables/nftables no son misteriosos. Son políticos. Demasiados actores, poca propiedad. Tu trabajo es parar la guerra eligiendo con claridad y haciéndolo cumplir.
- Decide el backend (nftables-first o iptables-legacy) y fíjalo usando alternatives/gestión de configuración.
- Verifica reenvío y NAT con contadores, no con sensaciones.
- Prueba la persistencia de reglas ante recargas de cortafuegos y reboots. Si las reglas desaparecen, arregla el orden o deja de hacer reemplazos completos del ruleset.
- Usa DOCKER-USER como tu hook estable de política. Mantén las cadenas de Docker como problema de Docker.
- Automatiza una comprobación post-boot que valide: backend, MASQUERADE presente, salto FORWARD a DOCKER-USER presente y al menos un contenedor sintético puede alcanzar un endpoint externo.
Si haces esas cinco cosas, no eliminarás incidentes de red—esto es Linux, no un cuento de hadas—pero eliminarás la clase de incidentes donde estás depurando el universo de cortafuegos equivocado.