Cortafuegos Linux: el diseño limpio de nftables que sigue legible con 500 reglas

¿Te fue útil?

No notas un cortafuegos hasta que duele. Normalmente a las 02:13, cuando “un cambio pequeño” convierte SSH en un fantasma,
tu teléfono de guardia en una alarma y a tu CEO en un ingeniero de red a tiempo parcial.

nftables puede ser elegante y rápido—pero solo si dejas de tratarlo como un iptables más grande.
Este es un diseño de producción que sigue legible cuando el conjunto de reglas supera las 500 reglas, sobrevive auditorías
y te permite depurar como un adulto.

Por qué el diseño importa más que la sintaxis

nftables no es difícil por su sintaxis. Es difícil porque tu conjunto de reglas es un sistema vivo:
crece por acumulación, se modifica bajo presión y lo leen personas que no lo escribieron
(incluido el Tú del Futuro, que es el revisor menos indulgente que existe).

Cuando cruzas ~200 reglas, pasan dos cosas:

  • El riesgo en los cambios se dispara. Un pequeño cambio puede mover el camino de un paquete de “accept” a “drop” sin una señal de diff obvia.
  • El tiempo de depuración explota. Dejas de razonar sobre la política y empiezas a hacer grep de cadenas y rezar.

La cura es la estructura. No la estructura de “comentarios por todas partes”. Estructura real:
límites de cadenas previsibles, nomenclatura estricta, sets/maps para identidad y un conjunto de reglas que refleje cómo fluyen realmente los paquetes.

Opinión: si tu archivo nftables es un blob único de 1.200 líneas, no tienes un cortafuegos. Tienes un reporte de incidente futuro.

Algunos hechos y contexto histórico (para que dejes de repetir errores de 2012)

Puntos cortos y concretos que importan en producción:

  1. nftables entró en el kernel Linux mainline en 3.13 (2014). Ya no es “nuevo”; tus malas costumbres sí lo son.
  2. iptables es en realidad una interfaz de netfilter. nftables es una interfaz más nueva, diseñada para reemplazar la familia iptables y reducir duplicación.
  3. nftables usa un enfoque tipo VM con bytecode. Por eso las reglas se pueden expresar de forma compacta y por qué los sets/maps reducen radicalmente el recuento de reglas.
  4. iptables históricamente tenía herramientas separadas para IPv4/IPv6/arptables/ebtables. nftables las unifica, lo cual es genial—hasta que “unificas” dos políticas que deberían permanecer separadas.
  5. Conntrack (seguimiento stateful) es anterior a nftables. nftables no inventó la statefulness; facilitó aplicarla de forma consistente.
  6. nftables soporta reemplazo atómico de ruleset. Esto es oro operacional: puedes cargar un nuevo ruleset como una transacción, no como una secuencia riesgosa de ediciones.
  7. Los sets fueron una revolución para rendimiento y legibilidad. En vez de 200 líneas “ip saddr X accept”, tienes una regla y una definición de set.
  8. nftables tiene mejores primitivas de introspección. Contadores, handles y listados estructurados están diseñados para depuración y tooling—no solo para humanos mirando texto.

Una cita que el equipo de operaciones debería tatuarse en su proceso de cambios:
La esperanza no es una estrategia. — General Gordon R. Sullivan

Principios de diseño que escalan más allá de 500 reglas

1) Un camino de paquete, una historia

Un conjunto de reglas legible cuenta la historia de un paquete. Por ejemplo: el paquete entrante golpea input → chequeos de sanidad →
established/related → “servicios permitidos” → registros con limitación → drop.

Si tu cadena input alterna entre “permitir nginx” y “bloquear bogons” y “permitir monitorización” y “drop fragments” sin orden,
has forzado al lector a simular toda la cadena en su cabeza. Eso no escala.

2) Denegar por defecto en los límites de cadena, no esparcido por todas partes

“Denegar por defecto” no es “dropear paquetes en 90 sitios”. Es una decisión deliberada de política al final de un camino
(o como política de cadena), con excepciones adelantadas.

Esparcir reglas drop al azar hace la auditoría imposible y crea “políticas en sombra” que nadie recuerda.

3) Usa sets/maps para identidad; usa cadenas para comportamiento

Los sets y maps son tu presupuesto de legibilidad. Los gastas para mantener las reglas cortas.
Las cadenas expresan comportamiento y orden. Si haces lo contrario (cadenas para identidad, reglas para comportamiento),
terminarás con un desastre combinatorio.

4) Separa “borde” de “host de servicio” de “tránsito”

La forma más rápida de crear un conjunto de reglas roto es mezclar lógica de enrutador (forwarding/NAT), lógica de host (servicios locales),
y “esta máquina también es endpoint VPN” en una sola cadena.

Usa tablas separadas o al menos archivos include separados. Haz que sea imposible cambiar NAT por accidente mientras “solo abres un puerto”.

5) Sé explícito sobre qué puede hablar con la propia máquina

La mayor parte del dolor en producción proviene del tráfico del plano de control: SSH, gestión de configuración, monitorización, sincronización horaria, descubrimiento de servicios.
Trátalo como una política de primera clase, no como un pensamiento posterior.

6) El registro debe ser útil o que no exista

Las tormentas de logs no “ayudan a depurar”. Ayudan a inflar tu factura SIEM.
Registra solo en puntos de decisión, limita la tasa y añade un prefijo que puedas grepear.

Broma #1: Si tu cortafuegos registra todo, felicidades—has inventado un generador de números aleatorios muy caro.

7) Prefiere recargas atómicas y prueba como si lo tomaras en serio

Los cambios en producción deberían ser: validar → dry-run (o al menos parse) → aplicar atómicamente → verificar contadores/comportamiento.
No “editar en vivo en un terminal y esperar que la sesión TCP aguante”.

8) No persigas micro-optimizaciones hasta que puedas explicar tu conjunto de reglas

nftables es rápido. Tu verdadero cuello de botella raramente es “dos comparaciones extra”, es “nadie sabe qué regla está activa”.
Optimiza para operabilidad primero. Tus incidentes futuros te lo agradecerán.

Diseño de referencia: archivos, tablas, cadenas y nomenclatura

Distribución de archivos (la parte que encantará a los auditores)

Mantén /etc/nftables.conf pequeño. Debe cargar tus reglas reales desde un directorio.
Esto facilita las revisiones, permite propiedad parcial (el equipo de red posee el archivo NAT, el equipo de plataforma posee el archivo de servicios),
y evita conflictos de merge que parecen espaguetis.

  • /etc/nftables.conf — punto de entrada
  • /etc/nftables.d/00-defs.nft — constantes, sets, maps, nombres de interfaces
  • /etc/nftables.d/10-filter-base.nft — cadenas base: esqueleto input/output/forward
  • /etc/nftables.d/20-filter-services.nft — permisos de servicios (ingreso a servicios locales)
  • /etc/nftables.d/30-filter-management.nft — SSH/monitorización/gestión de configuración
  • /etc/nftables.d/40-nat.nft — NAT (solo si es necesario)
  • /etc/nftables.d/90-debug.nft — reglas debug opcionales (deshabilitadas por defecto)

Convenciones de nombres que sobreviven a los equipos

Los nombres no son vanidad. Son la única forma de depurar rápidamente sin despertar a la persona que escribió las reglas hace tres años.
Usa prefijos previsibles:

  • Tablas: inet filter, ip nat (y ip6 nat solo si debes)
  • Cadenas base: input, output, forward
  • Cadenas de usuario: in_sanity, in_established, in_allow_mgmt, in_allow_services, in_log_drop
  • Sets: prefija con set_ y describe el contenido: set_mgmt_v4, set_bogon_v4, set_allowed_tcp_services
  • Maps: prefija con map_ y describe el mapeo: map_if_trust, map_service_ports

Por qué “inet filter” es la tabla por defecto

Usa table inet filter para el cortafuegos del host cuando sea posible. Obtienes una política para IPv4 e IPv6,
menos reglas duplicadas y menos incidentes de “olvidamos que IPv6 existe”.

Pero no seas dogmático: NAT suele estar todavía en las familias ip/ip6.
Mantén NAT separado del filtro. Mezclarlos es como guardar motosierras en el cajón de los cubiertos.

Estructura de cadenas: pequeñas, con propósito y aburridas

Las cadenas base deberían ser delgadas y mayormente solo saltar a subcadenas. Las subcadenas deberían tener cada una un trabajo.
Cuando haces esto, los contadores y trazas cobran significado: puedes ver en qué etapa se está perdiendo tráfico.

Ejemplo de conjunto de reglas limpio (con includes)

Este es un diseño que sigue legible con 500 reglas porque el conteo de reglas vive en sets, no en líneas repetidas.
También mantiene la depuración predecible porque el flujo de paquetes está por etapas.

cr0x@server:~$ sudo sed -n '1,160p' /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables.d/*.nft"
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/00-defs.nft
define IF_WAN = "eth0"
define IF_LAN = "eth1"
define IF_LOOP = "lo"

table inet filter {
  set set_mgmt_v4 {
    type ipv4_addr
    flags interval
    elements = { 198.51.100.10, 198.51.100.0/24 }
  }

  set set_mgmt_v6 {
    type ipv6_addr
    flags interval
    elements = { 2001:db8:100::/64 }
  }

  set set_bogon_v4 {
    type ipv4_addr
    flags interval
    elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16,
                 172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
  }

  set set_allowed_tcp_services {
    type inet_service
    elements = { 22, 80, 443, 9100 }
  }

  set set_allowed_udp_services {
    type inet_service
    elements = { 123 }
  }
}
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/10-filter-base.nft
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    iifname $IF_LOOP accept

    jump in_sanity
    jump in_established
    jump in_allow_mgmt
    jump in_allow_services
    jump in_log_drop
  }

  chain forward {
    type filter hook forward priority 0; policy drop;

    jump fwd_sanity
    jump fwd_established
    jump fwd_allow
    jump fwd_log_drop
  }

  chain output {
    type filter hook output priority 0; policy accept;

    jump out_sanity
  }

  chain in_sanity {
    ct state invalid drop

    ip saddr @set_bogon_v4 drop
    ip6 saddr ::/128 drop

    meta l4proto { tcp, udp, icmp, icmpv6 } accept
    drop
  }

  chain in_established {
    ct state { established, related } accept
  }

  chain in_log_drop {
    limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter
    drop
  }

  chain out_sanity {
    ct state invalid drop
  }

  chain fwd_sanity {
    ct state invalid drop
  }

  chain fwd_established {
    ct state { established, related } accept
  }

  chain fwd_log_drop {
    limit rate 10/second burst 20 packets log prefix "nft fwd drop " flags all counter
    drop
  }
}
cr0x@server:~$ sudo sed -n '1,260p' /etc/nftables.d/30-filter-management.nft
table inet filter {
  chain in_allow_mgmt {
    tcp dport 22 ip saddr @set_mgmt_v4 accept
    tcp dport 22 ip6 saddr @set_mgmt_v6 accept
  }
}
cr0x@server:~$ sudo sed -n '1,300p' /etc/nftables.d/20-filter-services.nft
table inet filter {
  chain in_allow_services {
    tcp dport { 80, 443 } accept
    tcp dport 9100 accept
    udp dport 123 accept
  }
}

Notas sobre el ejemplo:

  • Las cadenas base son cortas. Saltan a cadenas por etapas, lo que hace que el flujo de paquetes sea fácil de narrar.
  • Política drop en input/forward. Output es accept por defecto para la mayoría de servidores; ciérralo solo cuando tengas una razón.
  • La cadena de sanity es estricta. Descarta invalid temprano y maneja bogons. También tiene una “lista blanca de protocolos” para primitivas L4 para que lo absurdo se descarte rápido.
  • El tráfico de gestión está aislado. Las reglas de permiso SSH están en una cadena dedicada con restricciones de origen.
  • El registro está rate-limiteado y es tardío. Registras solo los drops al final de los caminos input/forward.

Broma #2: Las reglas de cortafuegos “temporales” tienen el mismo ciclo de vida que las tablas “temporales” en la base de datos: sobreviven a tu equipo.

Tareas prácticas: comandos, salidas y qué decides a partir de ellas

Estas son tareas reales que harás en máquinas reales. Cada una incluye el comando, una salida de aspecto realista,
lo que significa la salida y la decisión que tomas.

Task 1: Confirmar que nftables está activo y qué loader usa tu distro

cr0x@server:~$ systemctl status nftables
● nftables.service - nftables
     Loaded: loaded (/lib/systemd/system/nftables.service; enabled; preset: enabled)
     Active: active (exited) since Tue 2026-02-04 09:11:22 UTC; 2h 13min ago
       Docs: man:nft(8)
    Process: 612 ExecStart=/usr/sbin/nft -f /etc/nftables.conf (code=exited, status=0/SUCCESS)
   Main PID: 612 (code=exited, status=0/SUCCESS)

Significado: El servicio aplicó el ruleset y salió limpio (común en nftables).

Decisión: Si esto está inactivo/fallado, arregla la gestión del servicio primero; depurar reglas sin un loader consistente es caos.

Task 2: Volcar el ruleset activo tal como lo ve el kernel

cr0x@server:~$ sudo nft list ruleset
table inet filter {
	chain input {
		type filter hook input priority filter; policy drop;
		iifname "lo" accept
		jump in_sanity
		jump in_established
		jump in_allow_mgmt
		jump in_allow_services
		jump in_log_drop
	}
	chain in_allow_mgmt {
		tcp dport 22 ip saddr @set_mgmt_v4 accept
		tcp dport 22 ip6 saddr @set_mgmt_v6 accept
	}
	chain in_log_drop {
		limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter
		drop
	}
}

Significado: Esto es la verdad. No tu archivo, no tu repo git—el ruleset activo en el kernel.

Decisión: Si la salida no coincide con tus includes esperados, tienes un desajuste del loader o una implementación de configuración obsoleta.

Task 3: Validar la sintaxis antes de aplicarla (evitar bloquearte a ti mismo)

cr0x@server:~$ sudo nft -c -f /etc/nftables.conf

Significado: Sin salida y código de salida 0 significa que el parseo/chequeo tuvo éxito.

Decisión: Si la validación falla, arregla eso primero; no “lo pruebes en vivo”. Si debes aplicarlo de forma remota, la validación es innegociable.

Task 4: Aplicar cambios atómicamente y verificar el éxito

cr0x@server:~$ sudo nft -f /etc/nftables.conf

Significado: De nuevo, sin salida usualmente significa éxito.

Decisión: Sigue inmediatamente con comprobaciones de contadores (Task 7) y pruebas de accesibilidad de servicios. “Cargado” no es “correcto”.

Task 5: Mostrar handles de cadena (para eliminaciones precisas, auditorías y tooling)

cr0x@server:~$ sudo nft -a list chain inet filter input
table inet filter {
	chain input { # handle 1
		type filter hook input priority filter; policy drop;
		iifname "lo" accept # handle 5
		jump in_sanity # handle 6
		jump in_established # handle 7
		jump in_allow_mgmt # handle 8
		jump in_allow_services # handle 9
		jump in_log_drop # handle 10
	}
}

Significado: Los handles son identificadores estables para reglas en el ruleset activo.

Decisión: Si necesitas eliminar quirúrgicamente una regla durante un incidente, usa handles, no suposiciones de número de línea.

Task 6: Listar sets y confirmar que se cargaron como esperabas

cr0x@server:~$ sudo nft list set inet filter set_mgmt_v4
table inet filter {
	set set_mgmt_v4 {
		type ipv4_addr
		flags interval
		elements = { 198.51.100.0/24, 198.51.100.10 }
	}
}

Significado: Tus datos de identidad están presentes. Los sets con intervalos pueden agrupar entradas.

Decisión: Si un permiso de gestión depende de este set, confírmalo antes de culpar a “SSH” a la red.

Task 7: Ver contadores para ver qué se está golpeando realmente

cr0x@server:~$ sudo nft list chain inet filter in_log_drop
table inet filter {
	chain in_log_drop {
		limit rate 10/second burst 20 packets log prefix "nft in drop " flags all counter packets 41 bytes 2870
		drop
	}
}

Significado: 41 paquetes alcanzaron el logger de drop. No es teórico—algo está siendo denegado.

Decisión: Si los contadores suben tras un despliegue, biseca los cambios de política. Si los contadores están a cero pero los usuarios se quejan, el problema está en otro lado (enrutamiento, app, ACLs upstream).

Task 8: Verificar el comportamiento de conntrack (fuente común de bugs “funciona una vez”)

cr0x@server:~$ sudo conntrack -S
cpu=0 found=18231 invalid=12 ignore=0 insert=40121 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0

Significado: Tienes paquetes invalid (12). Algo de invalid es normal en Internet; números grandes sugieren enrutamiento asimétrico o offload roto.

Decisión: Si invalid sube rápidamente, investiga simetría de rutas y ajustes de offload; no “permitas invalid” solo para que los gráficos queden tranquilos.

Task 9: Usar nft monitor durante una ventana de cambio

cr0x@server:~$ sudo nft monitor
add rule inet filter in_allow_services tcp dport 8443 counter accept

Significado: Vista en vivo de reglas añadidas/eliminadas. Excelente para verificar que la automatización hizo lo que dice.

Decisión: Si ves agitación inesperada, para y revisa la herramienta de despliegue; los bucles de gestión de configuración pueden azotar tu ruleset.

Task 10: Rastrear la ruta de un paquete (la forma más rápida de encontrar “qué regla se lo comió”)

cr0x@server:~$ sudo nft add rule inet filter input meta nftrace set 1
cr0x@server:~$ sudo nft monitor trace
trace id 3c2a inet filter input packet: iif "eth0" ip saddr 203.0.113.55 ip daddr 198.51.100.20 tcp sport 51234 tcp dport 22
trace id 3c2a inet filter input rule jump in_sanity
trace id 3c2a inet filter in_sanity verdict accept
trace id 3c2a inet filter input rule jump in_established
trace id 3c2a inet filter input rule jump in_allow_mgmt
trace id 3c2a inet filter in_allow_mgmt verdict drop

Significado: Esta traza muestra la ruta de evaluación. El paquete no está en tu set de origen de gestión, así que no coincide con las reglas de accept y cae hasta el drop más adelante.

Decisión: O bien añades el origen a set_mgmt_v4 (si es legítimo) o le dices al usuario “no, así era la intención”.

Task 11: Vigilar los logs del kernel por drops rate-limited

cr0x@server:~$ sudo journalctl -k -n 5
Feb 04 11:22:19 server kernel: nft in drop IN=eth0 OUT= MAC=52:54:00:aa:bb:cc SRC=203.0.113.55 DST=198.51.100.20 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=53113 DF PROTO=TCP SPT=51234 DPT=22 WINDOW=64240 RES=0x00 SYN URGP=0
Feb 04 11:22:20 server kernel: nft in drop IN=eth0 OUT= MAC=52:54:00:aa:bb:cc SRC=203.0.113.8 DST=198.51.100.20 LEN=52 TOS=0x00 PREC=0x00 TTL=51 ID=0 DF PROTO=TCP SPT=60211 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0

Significado: Tu prefijo de registro funciona y puedes correlacionarlo con quejas de servicio.

Decisión: Si ves tráfico legítimo dropeado, arregla las reglas de allow. Si ves basura, mantén el registro mínimo y considera filtrado upstream.

Task 12: Confirmar que no olvidaste IPv6

cr0x@server:~$ ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
    inet6 2001:db8:200::20/64 scope global
       valid_lft forever preferred_lft forever

Significado: El host tiene una dirección IPv6 global. Si solo escribiste reglas IPv4, puedes haber expuesto servicios por v6—o bloqueado servicios inesperadamente.

Decisión: Usa la tabla inet para el filtro, confirma fuentes v6 en los sets de gestión y permite/niega servicios sobre IPv6 explícitamente según lo previsto.

Task 13: Confirmar que un puerto está escuchando antes de culpar al cortafuegos

cr0x@server:~$ sudo ss -lntp | head
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=811,fd=3))
LISTEN 0      4096   0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=1002,fd=6))
LISTEN 0      4096   0.0.0.0:443        0.0.0.0:*     users:(("nginx",pid=1002,fd=7))

Significado: Los servicios están escuchando en IPv4. Si los usuarios no pueden conectar, el cortafuegos puede ser culpable—o el enrutamiento/grupos de seguridad/ACLs upstream.

Decisión: Si no está escuchando, arregla el servicio. Si está escuchando, sigue con contadores/traza de nft y comprobaciones upstream.

Task 14: Detectar bloat de reglas contando reglas y buscando duplicados

cr0x@server:~$ sudo nft list ruleset | wc -l
892

Significado: El conteo de líneas es una métrica burda, pero es una prueba de olor. Si esperabas 250 líneas y obtuviste 892, probablemente duplicaste reglas o expandiste contenido generado.

Decisión: Convierte literales repetidos en sets/maps, separa includes por dominio y deja de generar casi-duplicados en la automatización.

Task 15: Confirmar que offload/fast path no está minando la visibilidad

cr0x@server:~$ ethtool -k eth0 | egrep 'gro|gso|tso|rx-checksumming|tx-checksumming'
rx-checksumming: on
tx-checksumming: on
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on

Significado: Los offloads están activados. Está bien, pero algunos entornos dan problemas con conntrack y ciertas encapsulaciones.

Decisión: Si ves invalid conntrack alto u trazas extrañas, prueba desactivar offloads específicos durante una ventana de mantenimiento—no como una superstición aleatoria.

Guion de diagnóstico rápido

Cuando estás bajo presión, no “revisas el ruleset”. Ejecutas un bucle cerrado que encuentra el cuello de botella rápidamente.
Aquí tienes una secuencia rápida que funciona para la mayoría de reportes “tráfico bloqueado” o “tráfico lento”.

Primero: prueba el servicio y el socket

  1. Comprueba si el proceso está escuchando en la IP/puerto esperado (ss -lntp).
  2. Confirma conectividad local (curl a localhost o nc -vz 127.0.0.1 PUERTO si aplica).
  3. Confirma que escucha en IPv6 si los clientes usan v6 (ss -lntp | grep '\[::\]:' patrones).

Si el servicio no escucha, el cortafuegos es inocente. Trata la inocencia como un activo valioso.

Segundo: prueba que los paquetes llegan al host

  1. Captura en la interfaz por una ventana corta (tcpdump -ni eth0 port 443).
  2. Si no llega nada, investiga enrutamiento, cortafuegos upstream, health checks del balanceador o grupos de seguridad.

Tercero: prueba que nftables es el punto de decisión

  1. Revisa contadores en tus cadenas de drop/log (nft list chain con contadores).
  2. Activa una traza de corta duración usando nftrace y nft monitor trace para localizar la etapa exacta.
  3. Confirma que sets/maps contienen los elementos esperados (IPs de gestión, puertos de servicio).

Cuarto: aisla la política específica rota

  1. ¿El tráfico es nuevo o existente? Si los flujos existentes funcionan pero los nuevos fallan, probablemente sea estado conntrack o un nuevo orden de reglas.
  2. ¿Es IPv4-only, IPv6-only o ambos? “Ambos” suele significar política de cadena; “uno” suele significar reglas faltantes para la familia o mal uso de inet.
  3. ¿Hay NAT involucrado? Si sí, revisa tablas NAT por separado; no persigas reglas de filtro por un bug de NAT.

Quinto: arregla con impacto mínimo

  1. Prefiere añadir a un set/map existente en lugar de añadir una regla aislada nueva.
  2. Prefiere añadir una regla allow en la cadena dedicada correcta (mgmt vs services) en lugar de en la cadena base.
  3. Recarga atómicamente y verifica con contadores y una prueba sintética.

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

1) Síntoma: SSH funciona desde la oficina pero no desde la VPN

Causa raíz: El allow de gestión está ligado a un set de origen que no incluye los rangos de egress de la VPN, o la VPN usa IPv6 y solo permitiste v4.

Solución: Añade la subred egress de la VPN a set_mgmt_v4/set_mgmt_v6. Valida con traza y luego recarga atómicamente.

2) Síntoma: “Funcionó tras reiniciar, luego falló más tarde”

Causa raíz: El estado conntrack permitió flujos establecidos, pero los flujos nuevos golpearon una regla deny nueva, o cambios basados en tiempo (DHCP, IPs dinámicas) movieron clientes fuera de sets permitidos.

Solución: Revisa la colocación de ct state; mantén established,related temprano. Para clientes dinámicos, usa identidad estable (pool de direcciones VPN, bastión) en lugar de rangos ISP inestables.

3) Síntoma: Exposición IPv6 o fallo IPv6 “de la nada”

Causa raíz: Escribiste reglas solo v4 mientras el host tiene v6 global, o usaste la tabla ip pensando que cubre ambas familias.

Solución: Usa table inet filter para el cortafuegos del host. Permite/niega servicios sobre v6 explícitamente. Verifica con ip -6 addr y nft list ruleset.

4) Síntoma: CPU alta en un borde con mucho tráfico tras “mejorar logs”

Causa raíz: Registrar demasiado temprano en la cadena, registrar accepts, o no limitar la tasa causa sobrecarga de logs en kernel y tormentas en espacio de usuario.

Solución: Registra solo drops finales, limita la tasa y usa prefijos concisos. Si necesitas observabilidad, usa contadores y trazas de forma quirúrgica.

5) Síntoma: Los drops de paquetes parecen aleatorios durante tráfico alto

Causa raíz: Presión en la tabla conntrack, picos de estado invalid, o enrutamiento asimétrico que hace que flujos establecidos parezcan invalid.

Solución: Revisa estadísticas de conntrack (conntrack -S), asegura simetría de rutas y no “acceptes invalid” como parche. Investiga offload y encapsulaciones.

6) Síntoma: La automatización sigue “arreglando” tu cambio manual de emergencia

Causa raíz: La gestión de configuración reaplica un estado deseado sin reconocer las ediciones de incidente.

Solución: Durante el incidente: actualiza el repo fuente de verdad rápidamente (incluso una rama hotfix), o pausa temporalmente el rol nftables en ese host. Después del incidente: codifica el cambio de emergencia correctamente (idealmente como elementos de set, no reglas ad-hoc).

7) Síntoma: NAT funciona para algunos hosts pero no para otros

Causa raíz: NAT y filtro mezclados de forma confusa; forward permite tráfico pero falta masquerade en postrouting o es demasiado estrecha.

Solución: Separa NAT en ip nat (y ip6 nat si es necesario). Verifica con capturas de paquetes y contadores. Mantén allows de forward y reglas NAT alineadas por identidad de interfaz/subred.

Tres micro-historias corporativas (porque reconocerás el olor)

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

Una empresa SaaS mediana migró de scripts iptables heredados a nftables usando una “traducción simple”.
El equipo supuso que el orden de sus reglas antiguas no importaba porque “son solo listas de permitidos”.
También supusieron que IPv6 era irrelevante porque su balanceador terminaba conexiones por IPv4.

El nuevo ruleset nftables entró en producción durante una tarde tranquila. Pasó la prueba básica:
el servicio web era accesible y SSH desde el bastión funcionaba. Todos se fueron a casa sintiéndose maduros.
Dos horas después, la monitorización empezó a mostrar fallos esporádicos desde un subconjunto de nodos en una región.

El problema real: esos nodos preferían IPv6 para el descubrimiento interno de servicios. El cortafuegos tenía una tabla de filtro ip,
y el tráfico IPv6 estaba golpeando un camino mayormente vacío. Algunos servicios quedaron expuestos por accidente; otros fueron silenciosamente bloqueados,
dependiendo de qué proceso se enlazara a [::].

La depuración llevó más tiempo del que debería porque el ruleset no estaba por etapas.
No había “cadena de gestión”, ni “cadena de servicios”, solo una larga lista de reglas mezcladas. Hacer trazas habría mostrado la ruptura en minutos,
pero nadie tenía un flujo de traza o un include de depuración seguro.

La solución fue aburrida: consolidar el cortafuegos del host en table inet filter, organizar el flujo por etapas
y hacer de IPv6 una decisión explícita en lugar de un efecto secundario accidental.
La línea más útil del postmortem fue: “IPv6 no es una característica. Es algo que ya existe.”

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

Un equipo de servicios financieros tenía una API de alto tráfico y quería exprimir latencia.
Alguien notó “demasiadas reglas” y decidió comprimir el cortafuegos llevando mucha lógica a mapas ingeniosos,
haciendo drops tempranos agresivos y registrando cada rechazo para “mejor visibilidad de seguridad”.

En papel, se veía genial: menos líneas en el ruleset y muchas coincidencias estructuradas.
En realidad, crearon un ruleset que nadie pudo leer durante un incidente.
El “map ingenioso” codificaba múltiples comportamientos (permitir, dropear, registrar) de forma que requería un compilador mental para entenderlo.

El fallo vino con la carga: el volumen de logs se disparó, el tiempo de kernel aumentó y la canalización SIEM empezó a retrasarse.
Bajo presión, alguien deshabilitó el registro por completo—eliminando una cadena compartida usada en tres sitios—porque el diseño no separaba responsabilidades.
Eso eliminó la visibilidad justo cuando el equipo la necesitaba.

La recuperación fue un rediseño: los maps se usaron solo para identidad (grupos de puertos y orígenes),
el comportamiento quedó en cadenas explícitas y el registro se movió a cadenas de drop al final del camino con límites estrictos.
La latencia mejoró ligeramente, pero la ganancia real fue operacional: el siguiente incidente duró 15 minutos en lugar de medio día.

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

Una gran empresa gestionaba miles de VMs Linux con nftables gestionado por configuración. Su política de seguridad cambiaba trimestralmente,
y cada trimestre había un temor: “este es el cambio que nos dejará fuera”.
Un equipo implementó en silencio una práctica que nadie celebraba: cada cambio de nftables tenía que validarse con nft -c,
desplegarse atómicamente y verificarse con contadores más una conexión sintética desde un host sondeador conocido.

Meses después, un cambio apresurado llegó tarde un viernes (porque claro que sí).
La nueva política eliminó por accidente un puerto UDP necesario para la sincronización horaria en un subconjunto de hosts.
El cambio se desplegó y en minutos, su pipeline de verificación señaló un pico en el contador de la cadena de drop y pruebas sintéticas fallando.

El on-call no tuvo que adivinar. Tenían un ruleset por etapas, así que los contadores apuntaron directamente a in_allow_services faltando UDP 123.
Actualizaron el set de puertos del servicio, re-validaron y recargaron atómicamente. El tiempo de inactividad fue limitado y contenido.
Nadie escribió un hilo épico en Slack. Así sabes que fue buena ingeniería.

La lección: la característica de cortafuegos más valiosa no es una expresión de coincidencia. Es un proceso disciplinado de cambios que asume que los humanos estarán cansados.

Listas de verificación / plan paso a paso

Paso a paso: construir un ruleset legible que escale

  1. Elige las familias de tabla: Usa inet para filter, separa ip/ip6 para NAT si hace falta.
  2. Define interfaces una vez: Usa define IF_WAN, IF_LAN, etc. Evita cadenas mágicas en las reglas.
  3. Crea sets de identidad: orígenes de gestión, orígenes de monitorización, subredes internas, puertos de servicios permitidos.
  4. Crea cadenas por etapas: sanity → established → management → services → log/drop.
  5. Mantén las cadenas base delgadas: Las cadenas base deberían saltar mayormente; no escondas política en ellas.
  6. Registra solo al final del camino: Un logger de drop por camino (input/forward), rate-limited, con prefijos consistentes.
  7. Haz cumplir el orden: Asegura que ct state established,related esté temprano. Los invalid se dropean temprano también.
  8. Usa recargas atómicas: Siempre despliega vía nft -f después de nft -c.
  9. Verifica con contadores: Confirma que las cadenas esperadas incrementan bajo tráfico; confirma que los contadores de drop se mantienen razonables.
  10. Mantén herramientas de depuración listas: Conserva un include de debug deshabilitado por defecto que puedas habilitar en emergencias (traza, logging temporal).

Checklist: seguridad previa al cambio para hosts remotos

  • Existe acceso fuera de banda (consola/ILO/IPMI/serial) o una ruta de bastión conocida está probada.
  • nft -c -f /etc/nftables.conf tiene éxito.
  • Tienes un método de rollback: la última configuración conocida buena disponible localmente.
  • No estás mezclando “refactor NAT” con “abrir un puerto”.
  • Tienes un comando de verificación en vivo listo (curl, nc, o un probe de monitorización).

Checklist: higiene continua (lo que mantiene legible 500 reglas)

  • Cada nueva regla allow debe caer en la cadena correcta (mgmt vs services vs transit).
  • Cada literal repetido (misma subred/grupo de puertos) se convierte en un set dentro de dos iteraciones.
  • Cada regla de log está rate-limited y tiene un prefijo acordado.
  • Los cambios de ruleset se revisan pensando en la “historia del paquete”: ¿puede un lector narrar el camino en 60 segundos?
  • Audita trimestralmente: poda puertos muertos, poda rangos de origen inactivos, colapsa duplicados en sets.

Preguntas frecuentes

1) ¿Debería usar política de cadena drop o una regla final drop explícita?

Para cadenas base como input y forward, una política de cadena drop es limpia y obvia.
Aun así, mantén una cadena explícita in_log_drop que registre y dropee—porque la política drop no registra por sí misma.

2) ¿Por qué no registrar cada drop en múltiples sitios?

Porque te ahogarás. Centraliza el registro al final del camino, limita la tasa y usa contadores en otros sitios.
Si necesitas detalle, habilita traza temporal por una ventana corta.

3) ¿Los sets siempre son más rápidos?

Usualmente sí—especialmente para listas largas de IPs/puertos. Pero la ganancia mayor es mantenibilidad.
Los sets también permiten actualizar la membresía sin reescribir la lógica de reglas.

4) ¿Dónde pongo el filtrado de “bogon”?

Ponlo en una cadena de sanity temprano en input (y forward si enrutas).
Mantén la lista como un set y revísala. No bloquees RFC1918 en interfaces internas a menos que disfrutes autoinfligirte outages.

5) ¿Cómo evito bloquear DNS/NTP/monitorización en salida por accidente?

Empieza con output policy accept para la mayoría de servidores.
Si debes restringir egress, hazlo como un proyecto separado con mapeo de dependencias completo y buena observabilidad.
Un bloqueo de egress sin inventario es una excelente forma de aprender cuántas dependencias “opcionales” tienes realmente.

6) ¿Cuál es la forma más segura de migrar de iptables a nftables?

No hagas una traducción a ciegas. Reexpresa la intención usando sets y cadenas por etapas.
Ejecuta lado a lado en un entorno controlado cuando sea posible, valida con pruebas de tráfico y luego corta con una carga atómica.

7) ¿Debería poner acceso de gestión y acceso a servicios en la misma cadena?

No. Sepáralos. La gestión es plano de control, los servicios son plano de datos.
Tienen diferentes restricciones de origen, diferentes requisitos de auditoría y diferentes respuestas ante incidentes.

8) ¿Cómo depuro “algunos clientes pueden conectar, otros no”?

Comprueba si los clientes que fallan comparten un rango de origen que no está incluido en tus sets.
Usa nft monitor trace para ver la discrepancia exacta. Confirma si los clientes usan IPv6.

9) ¿nftables es stateful por defecto?

No. Decides cómo usar conntrack state con coincidencias ct state.
La mayoría de cortafuegos de host en producción permiten established,related temprano y tratan invalid como drop.

10) ¿Cómo mantengo el ruleset legible cuando los equipos de producto exigen “solo una excepción más”?

Fuerza que las excepciones entren en datos (elementos de set) en lugar de lógica (nuevas reglas personalizadas).
Si la excepción cambia comportamiento, necesita su propia cadena con un nombre que explique qué es.
La vergüenza es una herramienta de gobernanza subestimada.

Próximos pasos que no arruinarán tu fin de semana

Un ruleset nftables legible no es una preferencia de estilo. Es una característica de fiabilidad.
A las 500 reglas, no estás luchando contra paquetes—estás luchando contra la entropía.

Haz esto a continuación:

  1. Divide tu monolito en includes: defs, base, mgmt, services, nat, debug.
  2. Convierte listas repetidas de IP/puertos en sets. Mantén identidad en sets; comportamiento en cadenas.
  3. Organiza el camino del paquete por etapas: sanity → established → allow mgmt → allow services → log/drop.
  4. Adopta el bucle de cambio: nft -c → carga atómica → verifica contadores → traza solo cuando sea necesario.
  5. Escribe tus convenciones de nombres y hazlas cumplir en las revisiones. Estás construyendo una herramienta para on-call, no un poema.

Si no haces nada más: haz obvia la historia del paquete. Cuando suene el pager, la claridad es la única métrica de rendimiento que importa.

← Anterior
WSL: La forma más rápida de tener un entorno de desarrollo real en Windows (sin el drama de VM)
Siguiente →
Picos de CPU cada pocos minutos: la tarea programada que debes revisar primero

Deja un comentario