IPv6 en Docker dentro de contenedores: actívelo correctamente (y evite fugas inesperadas)

¿Te fue útil?

Activa IPv6 en Docker porque el producto necesita “direcciones reales de internet”, o porque el agotamiento de IPv4 finalmente es tu problema.
Los contenedores arrancan, la mayoría de cosas funcionan… y entonces notas tráfico saliente que evita tu NAT IPv4 y tus controles de salida cuidadosamente diseñados.
O peor: un servicio es accesible por IPv6 desde lugares donde nunca debería serlo.

La historia de IPv6 en Docker es manejable, pero no es “activar y listo”. Debes decidir: IPv6 enrutado, NAT66 (sí, existe),
o “sin salida global salvo autorización explícita”. Luego debes aplicarlo con el firewall que realmente ejecutas, no con el que te gustaría ejecutar.

Qué sale mal con IPv6 en Docker (y por qué sorprende a los adultos)

El modelo mental por defecto de Docker es “los contenedores viven detrás de NAT IPv4 en un bridge”. Publicas puertos y el tráfico entrante funciona. El saliente funciona.
Configuras un par de reglas de iptables y tienes la sensación de control.

IPv6 cambia el contrato. Si el host tiene IPv6 global y entregas direcciones IPv6 globales (o enrutables globalmente) a los contenedores, ya no están
inherentemente detrás de NAT. Eso significa:

  • El control de salida puede fallar silenciosamente. Tus controles solo para IPv4 no se aplican a IPv6. El contenedor habla con el mundo por v6 sin problemas.
  • La exposición de entrada puede sorprenderte. Si permites reenvío por accidente (o Docker inserta reglas permisivas), los contenedores pueden ser accesibles.
  • Surgen brechas de observabilidad. Los registros de flujo, los registros de proxy y los paneles “qué IP usamos” a menudo asumen IPv4.
  • El DNS puede sortear la política. Respuestas AAAA + Happy Eyeballs pueden elegir IPv6 incluso cuando “tenías la intención” de usar IPv4.

Un giro más: Docker tiene múltiples lugares donde IPv6 puede estar “habilitado”, y no todos significan lo mismo.
Hay IPv6 a nivel de daemon, IPv6 por red, pools de direcciones, y luego lo que realmente hace el kernel y el firewall del host.
Cuando la gente dice “IPv6 está habilitado”, pregunta: “¿Dónde está habilitado y con qué política de salida?”

Hechos y contexto histórico (para calibrar tus instintos)

Algunos hechos pequeños y concretos ayudan a evitar grandes suposiciones costosas:

  1. IPv6 se estandarizó a finales de los 90 (era RFC 2460) en gran parte porque el agotamiento de IPv4 era un choque en cámara lenta que todos podían prever.
  2. El NAT nunca fue la característica de seguridad que se creyó. Ocultaba hosts, pero no reemplazó a un firewall; IPv6 quita esa “falsa manta de seguridad”.
  3. El enorme espacio de direcciones IPv6 cambió la mentalidad de asignación. La meta fue restaurar el direccionamiento end-to-end y reducir la traducción de direcciones con estado.
  4. Happy Eyeballs existe porque el despliegue de IPv6 fue desigual. Los clientes compiten IPv6 y IPv4 para evitar lentitud visible por el usuario cuando una ruta falla.
  5. Docker originalmente dependía mucho del comportamiento de iptables. A medida que los sistemas pasaron a nftables y a distintas distribuciones, la “magia” se volvió más frágil.
  6. Linux tiene soporte IPv6 robusto desde hace décadas, pero los sysctls por defecto y el comportamiento de RA varían según distro e imagen cloud.
  7. NAT66 existe pero es controvertido. Se usa para ciertas necesidades de transición y políticas, pero no es el “camino feliz” de IPv6.
  8. Muchas redes corporativas todavía bloquean IPv6 entrante por política mientras permiten IPv6 saliente, y eso es exactamente cómo ocurren las “fugas inesperadas”.
  9. IPv6 en la nube suele ser enrutado, no bridgeado. Con frecuencia obtienes un /64 (o más) enrutado a una interfaz; tu tarea es enrutar prefijos a las redes de contenedores limpiamente.

Un modelo mental sensato: puente L2, enrutamiento L3 y la “fuga” que no querías

La red bridge por defecto de Docker es algo así como L2 en el host: los contenedores obtienen una interfaz en un bridge de Linux y el host realiza el enrutamiento/reenvío L3.
Con IPv4, Docker normalmente SNATea el tráfico saliente hacia la IP del host (MASQUERADE). Así, el mundo ve al host, no al contenedor.

Con IPv6 tienes opciones. Si asignas direcciones IPv6 enrutables globalmente a los contenedores y enrutas ese prefijo apropiadamente, el mundo puede ver las direcciones de los contenedores.
Eso está bien—incluso es bueno—si eso era lo que querías y si aplicas firewall en consecuencia.
Es un problema cuando los “controles” de tu organización se implementaron como:

  • “Solo permitir salida a través de nuestro proxy IPv4.”
  • “Solo permitir entrada a puertos publicados.”
  • “Nuestro equipo de seguridad vigila los registros del gateway NAT.”

La fuga es simple: IPv6 se convierte en un segundo camino a internet menos gobernado. Tu contenedor puede resolver registros AAAA y conectar directamente.
Sin proxy. Sin gateway NAT. Sin registros donde esperabas encontrarlos.

Una cita para mantener en la pared, no porque sea poética, sino porque es operativamente cierta:
Idea parafraseada: “La esperanza no es una estrategia.” — atribuida a varios ingenieros y líderes en círculos de operaciones.

Opciones de diseño: IPv6 enrutado vs NAT66 vs sin-global-por-defecto

Decide lo que quieres. Si no lo haces, tu entorno decidirá por ti, generalmente durante una llamada de incidente.

Opción A: IPv6 enrutado (recomendado cuando puedes hacerlo con claridad)

Los contenedores obtienen direcciones de un prefijo que controlas. Enrutas ese prefijo desde tu enrutador upstream (o red cloud) al host Docker,
luego el host enruta a los bridges de contenedores. Sin NAT. End-to-end limpio. Fácil de razonar una vez que aceptas que el firewall es obligatorio.

Pros: IPv6 “real”, sin estado de traducción, mejor transparencia, se puede gestionar la entrada con firewall y servicios publicados.
Contras: requiere delegación de prefijos o subred enrutada; debes implementar firewall IPv6 de forma intencional.

Opción B: NAT66 (aceptable como parche de política, no como estilo de vida)

Puedes mantener la postura de “contenedores ocultos detrás del host” haciendo NAT para el egress IPv6.
No es “IPv6 puro”, pero puede ser un punto de control pragmático cuando el enrutamiento upstream es difícil o la política exige una identidad de salida única.

Pros: mantiene identidad de egress estable, reduce exposición entrante por defecto.
Contras: introduce estado de traducción, rompe supuestos end-to-end y puede confundir el troubleshooting.

Opción C: Sin IPv6 global para contenedores (solo ULA + egress controlado)

Da a los contenedores solo ULA (fd00::/8) internamente y fuerza la salida a través de un proxy o gateway que controlas.
Esto se alinea con “los contenedores no deberían hablar con internet salvo autorización explícita.”

Pros: control por defecto más estricto, menos exposición sorpresa.
Contras: tendrás que construir o ejecutar un gateway de todos modos; algunas apps esperan IPv6 directo.

Elige una y ponla por escrito. Hazla invariante de la plataforma. Si no, cada equipo inventará su propio enfoque IPv6 “solo esta vez”.

Habilitar IPv6 correctamente: daemon, redes y plan de direcciones

Hay dos grandes formas en que la gente se quema:

  • Habilitan IPv6 en Docker pero no planifican el prefijo, por lo que la selección de direcciones se vuelve accidental e inconsistente entre hosts.
  • Planifican el prefijo pero se olvidan del firewall, así que “IPv6 enrutado” se convierte en “zoológico público de contenedores”.

Planificación de direcciones que no te perseguirá

Quieres prefijos estables y no solapados por host Docker o por segmento de clúster. En un diseño de IPv6 enrutado, un patrón común es:

  • Asignar un prefijo más grande a tu entorno (por ejemplo, un /56 o /48 de tu proveedor).
  • Asignar un /64 por red bridge de Docker (porque SLAAC y muchas pilas asumen fronteras /64).
  • Enrutar esos /64 al host y luego dejar que Docker asigne direcciones dentro de ellos.

Si no puedes obtener espacio enrutable, ULA puede funcionar internamente, pero sé honesto: ULA no te da conectividad a internet automáticamente.
Aún necesitarás NAT66 o un proxy/gateway.

Controles del daemon de Docker que importan

La habilitación de IPv6 en Docker suele empezar en /etc/docker/daemon.json. Los ajustes exactos dependen de tus objetivos:

  • "ipv6": true habilita soporte IPv6 para el bridge por defecto.
  • "fixed-cidr-v6": "2001:db8:.../64" (ejemplo) asigna una subred IPv6 fija al bridge por defecto.
  • "ip6tables": true indica que Docker gestionará reglas de firewall IPv6. Esto puede ayudar, pero no externalices tu política al daemon.

Si ejecutas múltiples redes bridge definidas por el usuario, probablemente uses docker network create --ipv6 con subredes explícitas en lugar de depender únicamente del bridge por defecto.

Broma #1: IPv6 es como mudarte a una casa con muchas más habitaciones—genial hasta que te das cuenta de que olvidaste poner puertas.

Firewall para IPv6 en Docker: deja de confiar en sensaciones

Tu política debe existir en el firewall del host, no en una hoja de cálculo. Con IPv6, el host es un enrutador. Trátalo como tal.
Eso significa:

  • Política por defecto de bloqueo en el reenvío, luego permite explícitamente lo que quieres.
  • Política de salida explícita (especialmente si ejecutas proxies, controles DLP o gateways de auditoría).
  • Registro en los lugares correctos para que “por qué no se conecta” no sea un proyecto arqueológico de dos horas.

Realidad: iptables vs nftables

Muchas distribuciones ahora usan nftables internamente incluso cuando escribes iptables. Docker históricamente gestionaba reglas de iptables directamente.
Esto puede provocar comportamientos de “funcionaba el año pasado, ahora no” tras actualizaciones.

Tu trabajo: saber qué backend de firewall está activo y probar reglas con tráfico real.
Si usas nftables explícitamente, escribe reglas nftables. No confíes en Docker para mantenerte seguro.

Prevenir fugas inesperadas de egress IPv6

La fuga más común es: el egress IPv4 pasa por tu NAT/proxy; el egress IPv6 sale directo.
La solución es igual de simple y igual de ignorada: implementa controles de salida IPv6 en la ruta de reenvío del host.

Si tu política es “los contenedores deben usar proxy”, entonces tu firewall debe bloquear el egress IPv6 directo excepto hacia el proxy.
Si tu política es “los contenedores pueden hablar hacia afuera”, entonces permite eso, pero asegúrate de poder observarlo y limitar/segmentar si es necesario.

DNS y Happy Eyeballs: donde nace “funciona en mi portátil”

Una vez que IPv6 está disponible, el DNS devuelve registros AAAA. Muchos clientes preferirán IPv6 o competirán IPv6 e IPv4.
Eso significa que “bloqueamos IPv4 hacia ese destino” no es equivalente a “bloqueamos el destino”.

Revisa tus resolvers. Revisa las imágenes base de tus contenedores. Algunas imágenes incluyen ajustes en resolv.conf o tienen comportamiento diferente de la libc.
Si ejecutas DNS interno que sintetiza AAAA (DNS64), obtendrás egress IPv6 incluso cuando el servicio upstream sea solo IPv4.

Conclusión operativa: diagnostica la conectividad por ambas vías, A y AAAA, y no trates un “ping funciona” como prueba de alcance de la aplicación.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las tareas que realmente ejecuto cuando habilito IPv6 en Docker o lo depuro a las 2 a.m.
Cada una incluye: comando, ejemplo de salida, qué significa y la decisión que tomas.

Tarea 1: Confirmar que el host realmente tiene IPv6 y ruta por defecto

cr0x@server:~$ ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    inet6 2001:db8:10:20::15/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe4e:66a1/64 scope link
       valid_lft forever preferred_lft forever

Significado: Tienes un IPv6 global y uno link-local. Buen inicio.
Decisión: Si no hay IPv6 global, detente. No puedes hacer IPv6 enrutado sin soporte upstream. Considera ULA + gateway o arregla primero el IPv6 del host.

cr0x@server:~$ ip -6 route show default
default via 2001:db8:10:20::1 dev eth0 metric 100

Significado: El host tiene una ruta IPv6 por defecto.
Decisión: Si falta la ruta por defecto, los contenedores fallarán saliendo aunque tengan direcciones. Arregla el enrutamiento/RA/upstream antes de culpar a Docker.

Tarea 2: Verificar el reenvío del kernel y sysctls IPv6

cr0x@server:~$ sysctl net.ipv6.conf.all.forwarding net.ipv6.conf.default.forwarding
net.ipv6.conf.all.forwarding = 0
net.ipv6.conf.default.forwarding = 0

Significado: El host no está reenviando paquetes IPv6. Los contenedores pueden tener IPv6 pero no saldrán vía el host.
Decisión: Para IPv6 enrutado, pon forwarding=1 (y asegúrate de la política del firewall). Si tu intención es “no reenviar”, entonces esto es correcto—aplica control de salida de otra forma.

Tarea 3: Inspeccionar la configuración IPv6 del daemon Docker

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:dead:beef::/64",
  "ip6tables": true
}

Significado: IPv6 de Docker está activado en el bridge por defecto con un /64 fijo, y Docker gestionará reglas ip6tables.
Decisión: Valida que ese /64 esté enrutado al host (o al menos sea alcanzable). Si es aleatorio o se solapa, corrige el plan antes de reiniciar Docker.

Tarea 4: Reiniciar Docker y confirmar que aplicó la configuración

cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ systemctl status docker --no-pager -l
● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled)
     Active: active (running) since Tue 2026-01-02 09:18:31 UTC; 6s ago
     Docs: https://docs.docker.com
   Main PID: 1462 (dockerd)
     Tasks: 19
     Memory: 76.3M
     CGroup: /system.slice/docker.service
             └─1462 /usr/bin/dockerd -H fd://

Significado: Docker se reinició correctamente.
Decisión: Si falla al iniciar, revisa los logs del journal por errores de sintaxis JSON u opciones no soportadas en tu versión de Docker.

Tarea 5: Verificar que el bridge por defecto tiene una subred IPv6

cr0x@server:~$ docker network inspect bridge --format '{{json .IPAM.Config}}'
[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"},{"Subnet":"2001:db8:dead:beef::/64","Gateway":"2001:db8:dead:beef::1"}]

Significado: El bridge por defecto ahora tiene un /64 IPv6 y una dirección de gateway.
Decisión: Si falta la subred IPv6, tu configuración del daemon no se aplicó o IPv6 está deshabilitado. Corrige antes de continuar.

Tarea 6: Crear una red definida por usuario con IPv6 explícito

cr0x@server:~$ docker network create --driver bridge --ipv6 --subnet 2001:db8:dead:cafe::/64 appv6
a1d8d3f6a2e7b2c1c0c9b5c0d74a2c1c4f6a8a1c2e1f0b9e8d7c6b5a4f3e2d1

Significado: Ahora tienes una red bridge con IPv6 y un /64 explícito. Esto suele ser más manejable que el bridge por defecto.
Decisión: Si tu organización quiere segmentación por app, crea una por stack y aplica política de firewall por interfaz bridge.

Tarea 7: Ejecutar un contenedor y confirmar que recibe IPv6

cr0x@server:~$ docker run --rm --network appv6 alpine sh -c "ip -6 addr show dev eth0"
27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    inet6 2001:db8:dead:cafe::2/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever

Significado: El contenedor tiene un IPv6 global desde tu subred y una dirección link-local.
Decisión: Si solo tiene link-local, IPv6 de Docker no está correctamente habilitado para esa red o la subred no se aplicó.

Tarea 8: Probar conectividad IPv6 saliente desde el contenedor

cr0x@server:~$ docker run --rm --network appv6 alpine sh -c "apk add --no-cache curl >/dev/null; curl -6 -sS -m 3 https://ifconfig.co/ip"
2001:db8:10:20::15

Significado: El egress IPv6 funciona y la salida aparece como la IP del host (en este ejemplo). Dependiendo del enrutamiento/NAT, podría mostrarse la IP del contenedor o la del host.
Decisión: Si la política exige egress vía proxy, este “IPv6 directo” es una fuga. Bloquéalo o enrútalo explícitamente por el proxy.

Tarea 9: Ver si Docker instaló reglas de firewall IPv6 (y si son sensatas)

cr0x@server:~$ sudo ip6tables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j RETURN

Significado: DOCKER-USER existe pero está vacío (RETURN). Docker no impondrá tu política por defecto.
Decisión: Pon tu política de egress/ingress de contenedores en DOCKER-USER (o su equivalente nftables) para que sobreviva a las reglas dinámicas de Docker.

Tarea 10: Comprobar si el firewall permite reenvío (ruta nftables)

cr0x@server:~$ sudo nft list ruleset | sed -n '1,120p'
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    iif "lo" accept
    ct state established,related accept
  }
  chain forward {
    type filter hook forward priority 0; policy drop;
    ct state established,related accept
  }
  chain output {
    type filter hook output priority 0; policy accept;
  }
}

Significado: Default-drop en forward. Buena línea base: nada transita a menos que lo permitas. Docker aun puede insertar reglas en otros lugares dependiendo de la configuración.
Decisión: Añade permisos explícitos para las interfaces bridge de Docker hacia los destinos que quieras. Si tu cadena forward es accept-por-defecto, espera “exposición sorpresa”.

Tarea 11: Confirmar qué interfaces creó Docker y mapearlas a redes

cr0x@server:~$ ip link show type bridge
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
8: br-7b3b9c2f7a12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500

Significado: docker0 (bridge por defecto) y un bridge definido por usuario (br-…).
Decisión: Usa reglas de firewall por bridge. Si unificas todo en una misma lista de permitidos, pierdes segmentación y claridad al depurar.

Tarea 12: Verificar que existen rutas al subnet de contenedores en el host

cr0x@server:~$ ip -6 route show | grep -E 'dead:beef|dead:cafe'
2001:db8:dead:beef::/64 dev docker0 proto kernel metric 256
2001:db8:dead:cafe::/64 dev br-7b3b9c2f7a12 proto kernel metric 256

Significado: El host tiene rutas conectadas a /64s de contenedores vía los bridges Docker.
Decisión: Si faltan estas rutas, la configuración de red de Docker es incorrecta o el bridge no está activo. Corrige antes de tocar el enrutamiento upstream.

Tarea 13: Confirmar alcance upstream al subnet de contenedores (el enrutamiento no es opcional)

cr0x@server:~$ ping6 -c 2 2001:db8:dead:cafe::2
PING 2001:db8:dead:cafe::2(2001:db8:dead:cafe::2) 56 data bytes
64 bytes from 2001:db8:dead:cafe::2: icmp_seq=1 ttl=64 time=0.120 ms
64 bytes from 2001:db8:dead:cafe::2: icmp_seq=2 ttl=64 time 0.118 ms

--- 2001:db8:dead:cafe::2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1010ms

Significado: Desde el host al contenedor está bien (local). Esto no prueba que el mundo exterior pueda alcanzar el subnet de contenedores.
Decisión: Si necesitas ingreso hacia contenedores, asegúrate de que tu enrutador upstream tenga rutas para estos /64s apuntando al host.

Tarea 14: Detectar “IPv6 elude el proxy” en un minuto

cr0x@server:~$ docker run --rm --network appv6 alpine sh -c "apk add --no-cache bind-tools curl >/dev/null; nslookup example.com | sed -n '1,12p'"
Server:         127.0.0.11
Address:        127.0.0.11:53

Non-authoritative answer:
Name:   example.com
Address: 93.184.216.34
Name:   example.com
Address: 2606:2800:220:1:248:1893:25c8:1946

Significado: El contenedor ve tanto A como AAAA. Muchos clientes usarán IPv6 si funciona.
Decisión: Si tu postura de seguridad asume logging por proxy/NAT, implementa controles de salida IPv6 ahora, no después de que auditoría pregunte por qué datos salieron fuera de control.

Broma #2: Si no estás firewallando IPv6, básicamente operas producción con la puerta mosquitera en “modo demo”.

Guión rápido de diagnóstico

Cuando IPv6 en contenedores está roto (o demasiado “funcionando”), quieres un camino corto hacia la verdad.
Aquí está el orden que encuentra el cuello de botella rápidamente.

Primero: salud IPv6 a nivel host (no empieces dentro del contenedor)

  • ¿El host tiene una dirección IPv6 global en la interfaz uplink?
  • ¿Hay una ruta IPv6 por defecto?
  • ¿Está habilitado el reenvío IPv6 si vas a enrutar tráfico de contenedores?

Si el IPv6 del host es inestable, el IPv6 de los contenedores es solo teatro.

Segundo: cableado de red de Docker

  • ¿La red Docker tiene una subred IPv6 y un gateway?
  • ¿El contenedor obtiene una dirección IPv6 global/ULA en eth0?
  • ¿El host tiene una ruta conectada a esa subred vía el bridge?

Tercero: firewall y política (el culpable habitual)

  • ¿Se permiten paquetes IPv6 reenviados desde el bridge de contenedores hacia el uplink?
  • ¿Se bloquea ICMPv6 incorrectamente (rompe PMTU, discovery de vecinos y la cordura general)?
  • ¿Tu política de egress está implementada para IPv6, o solo para IPv4?

Cuarto: DNS y comportamiento de la aplicación

  • ¿El DNS devuelve AAAA? ¿La app usa IPv6 inesperadamente?
  • ¿Hay DNS64 o un resolver interno que reescribe respuestas?
  • ¿Ves comportamiento distinto con curl -4 vs curl -6?

Quinto: enrutamiento upstream (solo si necesitas ingreso)

  • ¿Están presentes las rutas para los /64 de contenedores en el enrutador upstream/tabla de rutas cloud?
  • ¿El filtrado de ruta inversa o anti-spoofing está descartando tráfico?

Errores comunes: síntoma → causa raíz → solución

Estos no son “teóricos”. Son los que aparecen en las líneas de tiempo de incidentes.

1) Los contenedores tienen direcciones IPv6 pero no alcanzan nada por IPv6

Síntoma: curl -6 falla por timeout; DNS muestra AAAA; IPv4 funciona.
Causa raíz: Reenvío IPv6 del host deshabilitado, o la cadena forward del firewall descarta IPv6.
Solución: Habilita net.ipv6.conf.all.forwarding=1 para diseños enrutados y permite reenvío para la interfaz bridge de Docker; mantén default-drop pero añade permisos específicos.

2) El egress IPv6 evade el proxy/NAT de la empresa

Síntoma: El equipo de seguridad ve tráfico a destinos IPv6 externos que no aparecen en los registros NAT IPv4.
Causa raíz: No hay política de salida para IPv6; los clientes prefieren IPv6 por AAAA + Happy Eyeballs.
Solución: Bloquea el egress IPv6 directo desde bridges de contenedores salvo hacia gateways/proxies aprobados, o fuerza el uso de proxy; valida con pruebas curl -6.

3) Un contenedor es alcanzable desde internet por IPv6 sin publicar puertos

Síntoma: Un escaneo externo detecta un servicio escuchando en el IPv6 del contenedor; el equipo jura que no lo publicaron.
Causa raíz: IPv6 enrutado + reglas de reenvío/input permisivas permiten entrada; Docker no “oculta” servicios como lo hacía NAT en IPv4.
Solución: Default-drop en inbound/forwarded IPv6; permite explícitamente solo los puertos necesarios a contenedores específicos; considera redes separadas para workloads públicos.

4) Las cosas funcionan en un host pero no en otro

Síntoma: Mismo compose, diferente comportamiento entre nodos.
Causa raíz: Backend de firewall distinto (iptables vs nft), sysctls diferentes, o provisión IPv6 upstream distinta (algunos nodos tienen rutas v6 y otros no).
Solución: Estandariza la base de los hosts: sysctls, herramienta de firewall, daemon.json y validación de provisión IPv6 en CI para imágenes de nodos.

5) Stalls aleatorios y problemas MTU raros sobre IPv6

Síntoma: Transferencias grandes se cuelgan; solicitudes pequeñas funcionan; logs muestran retransmisiones.
Causa raíz: ICMPv6 bloqueado (PMTU discovery falla), o desajuste de MTU en overlay/underlay.
Solución: Permite tipos esenciales de ICMPv6; verifica path MTU; evita reglas que “bloquean todo ICMP” heredadas de blogs de 2004.

6) “Habilitamos IPv6” pero solo existen direcciones link-local

Síntoma: El contenedor muestra solo direcciones fe80::.
Causa raíz: La red no se creó con --ipv6 o IPv6 a nivel daemon no está activado; falta fixed-cidr-v6 para el bridge por defecto.
Solución: Habilita IPv6 en el daemon y/o por red; recrea redes según sea necesario (con cuidado, en ventana de mantenimiento).

Tres mini-historias corporativas desde las trincheras IPv6

Mini-historia 1: El incidente causado por una suposición errónea

Una compañía SaaS mediana migró un servicio de ingesta a contenedores en una flota de hosts Linux. El egress IPv4 estaba estrictamente controlado:
todo el tráfico saliente de las cargas pasaba por un gateway NAT monitorizado y una capa de proxy. Auditoría estaba contenta. Finanzas también. Todos dormían.

El equipo de plataforma habilitó IPv6 en los hosts porque una API de un nuevo partner era “IPv6-first”, y querían evitar otra capa de traducción en el gateway.
Activaron la configuración IPv6 de Docker, validaron que los contenedores alcanzaban al partner por IPv6 y siguieron con otras tareas.

Dos semanas después, seguridad alertó de conexiones salientes inusuales hacia destinos externos que no aparecían en los registros del gateway NAT.
No era malware. Era telemetría normal de aplicaciones, directa a endpoints de proveedores, que eligió IPv6 porque el proveedor tenía registros AAAA.
Nadie había decidido si la telemetría podía eludir el proxy. Simplemente lo hizo.

La respuesta al incidente fue incómoda porque nada “había sido hackeado”. El sistema hizo exactamente lo que se configuró.
La suposición equivocada fue cultural: se pensaba “NAT equivale a control de salida”. IPv6 quitó el NAT y, por tanto, la ilusión.

La solución fue directa pero políticamente delicada: implementaron reglas explícitas de egress IPv6 en los hosts Docker y forzaron HTTP(S) saliente
a través de un proxy controlado, para IPv4 e IPv6. La lección no fue sobre Docker; fue que la política de red debe ser explícita por familia de protocolos.

Mini-historia 2: La optimización que salió mal

Una gran empresa tenía una plataforma de contenedores donde IPv6 estaba “soportado”, pero solo de forma limitada: ULA interna y NAT66 en el borde.
El equipo de networking quiso reducir estado y simplificar el troubleshooting, así que propusieron “IPv6 enrutado por todas partes”.
Suena limpio. Lo es, cuando se hace deliberadamente.

Desplegaron /64 enrutados por host y permitieron reenvío de forma amplia porque no querían romper cargas.
El plan era endurecer reglas de firewall después, una vez observaran patrones de tráfico típicos.
Ese “después” no se agendó. Fue solo una actitud.

En un mes, un escaneo de vulnerabilidades encontró varias UIs administrativas internas alcanzables por IPv6 desde redes adyacentes.
No estaban “expuestas a internet”, pero sí más allá del segmento previsto, lo que bastó para desencadenar una crisis de cumplimiento.
Los servicios nunca habían sido alcanzables por IPv4 debido al NAT y al deny-por-defecto en esa dirección, así que sus propietarios no los consideraban riesgosos.

Lo que salió mal no fue IPv6 enrutado. IPv6 enrutado era la arquitectura correcta. Lo que falló fue el “firewall permisivo temporal”.
Optimizar sin guardarraíles es simplemente acelerar hacia un postmortem.

Terminaron haciendo el trabajo que debieron haber hecho primero: default-drop en forwarding, permisos explícitos por red y puerto, y una separación formal entre redes públicas y privadas.
IPv6 enrutado quedó. El hábito de “endurecer después” no.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una empresa relacionada con pagos ejecutaba cargas con control de cambios estricto. No divertido, pero eficaz.
Al añadir IPv6, escribieron un estándar de una página: plan de direcciones, asignación por host y invariantes de firewall.
Cada build de host validaba los mismos sysctls y reglas de firewall. Cada red Docker se creaba explícitamente con un /64 conocido.

Meses después, una actualización de imagen cloud cambió sutilmente el comportamiento del firewall: la distro pasó a nftables con una política por defecto distinta.
En una plataforma menos disciplinada, aquí es donde IPv6 dejaría de funcionar en silencio o se abriría sin aviso.
Aquí, las pruebas de validación fallaron durante el bake: los contenedores perdieron egress IPv6 porque faltaban reglas de reenvío.

La corrección fue aburrida: actualizar la plantilla de nftables, volver a ejecutar la validación, volver a bakear.
Sin incidente. Sin cambio de emergencia. Sin “¿por qué solo IPv6 falla?”

La práctica que los salvó fue también la menos glamurosa: tratar IPv6 como ciudadano de primera clase en las pruebas de configuración base.
Si solo pruebas IPv4 en tu pipeline de imagen dorada, estás eligiendo sorprenderte más tarde.

Listas de verificación / plan paso a paso

Aquí está el plan que usaría para desplegar IPv6 en Docker en producción donde un “ups” cuesta dinero.
Elige la opción de diseño primero y luego ejecuta.

Checklist A: Prevuelo (host y upstream)

  1. Confirma que el host tiene IPv6 global estable y una ruta por defecto.
  2. Decide: IPv6 enrutado, NAT66 o solo ULA.
  3. Obtén o asigna un plan de prefijos (por host o por red). Evita solapamientos entre entornos.
  4. Configura sysctls: habilita reenvío IPv6 si vas a enrutar contenedores; asegura que el comportamiento de RA coincida con tu entorno.
  5. Define la base del firewall: default-drop en forwarding; permite ICMPv6 requerido; decide controles de salida.

Checklist B: Configuración de Docker

  1. Configura /etc/docker/daemon.json con "ipv6": true y un "fixed-cidr-v6" si usas el bridge por defecto.
  2. Prefiere redes definidas por usuario con subredes IPv6 explícitas para las apps.
  3. Reinicia Docker en ventana de mantenimiento si afecta workloads en ejecución.
  4. Valida que docker network inspect muestre subred/gateway IPv6.

Checklist C: Aplicación de políticas (evitar fugas)

  1. Implementa política de egress IPv6: permite solo lo que quieres (proxy, destinos específicos o salida completa).
  2. Implementa política de ingress IPv6: default-drop, permite solo servicios publicados y asegúrate de que los subnets de contenedores no sean ampliamente alcanzables.
  3. Registra descartes de forma rate-limited para depurar sin llenar disco.
  4. Prueba comportamiento dirigido por AAAA: compara curl -4 y curl -6 desde contenedores.

Checklist D: Puertas de validación antes del despliegue

  1. Conectividad: contenedor a internet por IPv6 (si está permitido) y a dependencias internas.
  2. Prueba de fuga: confirma que el egress IPv6 directo está bloqueado si la política requiere proxy.
  3. Prueba de exposición: confirma que los contenedores no son alcanzables por IPv6 salvo cuando se pretende.
  4. Observabilidad: confirma que logs/ métricas incluyen IPv6 (parsing de IP cliente, dashboards, alertas).
  5. Prueba de actualización: asegura que cambios en herramientas de firewall (iptables/nftables) se detecten en la validación de imagen.

Preguntas frecuentes

1) ¿Debo habilitar IPv6 en Docker globalmente o por red?

Por red, con subredes explícitas, suele ser más limpio. Habilita a nivel de daemon para permitirlo y luego crea redes definidas por usuario con --ipv6.
Te lo agradecerás cuando necesites segmentación y direccionamiento predecible.

2) ¿Necesito un /64 para cada red Docker?

Prácticamente, sí si quieres evitar sorpresas. Muchas herramientas y suposiciones de IPv6 giran alrededor de /64. Prefijos más pequeños pueden funcionar en algunos diseños enrutados,
pero pasarás tiempo luchando con casos extremos en lugar de ejecutar servicios.

3) ¿Es NAT66 “malo”?

No es un fallo moral. Es un trade-off. Si tu enrutamiento upstream está limitado o necesitas una identidad de salida única, NAT66 puede ser pragmático.
Solo documéntalo y espera cierta complejidad en el troubleshooting.

4) ¿Por qué mis controles de egress dejaron de funcionar después de habilitar IPv6?

Porque tus controles eran específicos de IPv4 (registros del gateway NAT, reglas de firewall IPv4, proxies que solo aplican IPv4). IPv6 creó un segundo camino.
Arregla aplicando políticas explícitas para IPv6—no esperes que los clientes sigan usando IPv4.

5) ¿Pueden los contenedores ser alcanzables por IPv6 sin publicar puertos?

Sí, en diseños IPv6 enrutados, si el firewall lo permite y existe enrutamiento. NAT IPv4 solía enmascarar esta realidad.
Con IPv6, asume que un socket en escucha es accesible salvo que lo bloquees.

6) ¿Necesito permitir ICMPv6? ¿No puedo simplemente bloquearlo como hicimos con ICMP?

Necesitas ICMPv6 esencial para discovery de vecinos y PMTU. Bloquear en exceso ICMPv6 provoca fallos intermitentes raros que consumen fines de semana.
Permite tipos específicos; no descartes todo ICMP a la ligera.

7) ¿Por qué curl usa IPv6 a veces aunque IPv4 funcione?

El DNS devuelve registros AAAA y las librerías cliente suelen preferir IPv6 o competir IPv6/IPv4 (Happy Eyeballs).
Si IPv6 está disponible y no está bloqueado, se usará. Trátalo como comportamiento esperado, no como traición.

8) ¿Cómo prevengo fugas IPv6 sin romper todo?

Empieza bloqueando IPv6 saliente desde las interfaces bridge de contenedores salvo hacia gateways conocidos (proxy, mirrors de actualizaciones si hace falta).
Luego añade excepciones de forma intencional. Valida con curl -6 y comprobaciones de AAAA en DNS.

9) ¿Docker Compose soporta redes IPv6?

Sí, definiendo redes con IPv6 habilitado y subredes explícitas. La clave es asegurar que el daemon y el host soporten IPv6 y que la política de firewall sea coherente.

10) ¿Cuál es la prueba más rápida de que un contenedor está “escapando” por IPv6?

Resuelve un dominio dual-stack conocido y fuerza IPv6: usa nslookup para ver AAAA, luego curl -6 a un endpoint público.
Si tiene éxito mientras tus controles IPv4 lo habrían bloqueado, tienes una brecha de política de egress.

Conclusión: próximos pasos que no te avergüencen en dos trimestres

Habilitar IPv6 en Docker no es difícil. Habilitarlo de forma segura es disciplina.
El trabajo no es mayormente “Docker”. Es enrutamiento y política: planificación de direcciones, reenvío, firewall, comportamiento DNS y observabilidad.

Pasos prácticos siguientes:

  1. Escribe tu elección de diseño (IPv6 enrutado, NAT66 o ULA-only) y la postura de seguridad asociada.
  2. Implementa checks base en hosts: dirección IPv6, ruta por defecto, sysctls de reenvío y consistencia del backend de firewall.
  3. Crea redes IPv6 explícitas en Docker con /64 conocidos; deja de permitir que “defaults” sean arquitectura.
  4. Aplica política de egress IPv6 en la ruta de reenvío para que IPv6 no pueda eludir tus controles.
  5. Añade validación en tu pipeline de imágenes: ejecuta pruebas curl -6, comprobaciones AAAA de DNS y un escaneo mínimo de exposición en staging.

Haz eso, y IPv6 se convertirá en solo otro transporte que tu plataforma soporta—aburrido, predecible y sin un canal lateral sorpresa hacia internet.

← Anterior
Portátil i7, lento como una patata: cuando los límites de potencia se ríen de los compradores
Siguiente →
WordPress .htaccess rompió el sitio: restaurar un valor predeterminado seguro correctamente

Deja un comentario