Firewall en Linux: de iptables a nftables

Netfilter es el subsistema del kernel que intercepta paquetes de red. Todo firewall en Linux —sin excepción— opera sobre él. Lo que cambia entre herramientas es únicamente la interfaz en espacio de usuario que usas para programar esas reglas. Entender esto primero evita confusión cuando ves que iptables, nftables y ufw coexisten en el mismo sistema.

La arquitectura de iptables

iptables organiza las reglas en tres niveles: tablas, chains y reglas individuales. Las tablas agrupan funcionalidad por propósito: filter decide si un paquete pasa o se descarta, nat modifica direcciones, mangle altera cabeceras, raw trabaja antes del tracking de conexiones. Dentro de cada tabla, las chains representan puntos de enganche en el flujo del paquete: INPUT para tráfico destinado al propio sistema, OUTPUT para el que genera, FORWARD para el que transita, PREROUTING y POSTROUTING para manipulaciones antes y después del enrutamiento.

Cuando escribes iptables -A INPUT -p tcp --dport 22 -j ACCEPT, estás añadiendo una regla al final (append) de la chain INPUT en la tabla filter por defecto. El kernel evalúa las reglas en orden secuencial hasta encontrar una coincidencia. Si ninguna coincide, se aplica la política por defecto de la chain.

El problema real de iptables no es conceptual: es operacional. Primero, no hay atomicidad: cuando aplicas un conjunto de reglas nuevo, cada línea es una operación independiente. Durante esa ventana el firewall está en un estado intermedio. Segundo, iptables solo maneja IPv4; necesitas ip6tables para IPv6, duplicando el trabajo y multiplicando las oportunidades de inconsistencia. Tercero, las reglas no persisten tras un reinicio sin herramientas adicionales como iptables-persistent. En producción, estos tres problemas combinados son una fuente constante de incidentes.

nftables: el reemplazo

nftables entró en el kernel en 3.13 (2014) y en Debian se convirtió en el backend por defecto a partir de Debian 10. La diferencia fundamental no es solo sintáctica: nftables permite reemplazar el ruleset completo de forma atómica con una sola operación. O el conjunto nuevo entra todo de golpe, o no entra nada. Esto elimina la ventana de inconsistencia.

La otra ventaja concreta: una misma regla puede filtrar IPv4 e IPv6 simultáneamente usando la familia inet. No hay duplicación.

El detalle que más confusión genera en Debian hoy: cuando ejecutas iptables en Bookworm, no estás usando el binario original basado en setsockopt. Estás usando iptables-nft, un frontend que traduce la sintaxis clásica a reglas nftables. Puedes verificarlo:

update-alternatives --display iptables

Verás que apunta a /usr/sbin/iptables-nft. Esto tiene una implicación importante: las reglas que creas con iptables y las que creas con nft directamente comparten el mismo backend, y puedes verlas todas juntas con nft list ruleset. Si mezclas los dos frontends sin saberlo, puedes acabar con reglas que se contradicen o se solapan de formas no obvias.

El stack recomendado en producción

Para la mayoría de servidores Debian, el stack es: nftables directamente (o UFW por encima si prefieres abstraer la complejidad) con las reglas persistidas en /etc/nftables.conf. iptables directo tiene sentido en dos casos: herramientas legacy que lo requieren explícitamente (ciertos módulos de Kubernetes antiguos, algunos sistemas de VPN con configuración hardcodeada) o cuando necesitas compatibilidad con scripts que ya tienes y no vale la pena reescribir ahora.

El siguiente ejemplo construye un firewall funcional para un servidor con SSH y HTTPS, usando nftables directamente. Después añadimos una regla de NAT para un escenario de gateway, y verificamos que todo es consistente:

# Verificar el estado actual antes de tocar nada
nft list ruleset

# El archivo de configuración canónico en Debian
# Editar directamente en lugar de aplicar reglas sueltas
cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f

# Limpiar todo lo que haya antes — esto es parte del atomic load
flush ruleset

# Familia inet cubre IPv4 e IPv6 en la misma tabla
table inet filter {

    # Conjuntos reutilizables: más eficiente que múltiples reglas individuales
    # nftables evalúa sets con hash/btree, no linealmente
    set allowed_tcp {
        type inet_service
        elements = { 22, 443 }
    }

    chain input {
        # Política por defecto: DROP — todo lo que no coincida explícitamente se descarta
        type filter hook input priority 0; policy drop;

        # Tráfico de loopback siempre permitido
        iif lo accept

        # Conexiones establecidas y relacionadas: no re-evaluar el estado completo
        ct state established,related accept

        # Descartar explícitamente paquetes inválidos antes de procesar más
        # Evita ciertos ataques de reconocimiento y reduce carga en las reglas siguientes
        ct state invalid drop

        # ICMP necesario para diagnóstico y Path MTU Discovery
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Referencia al set: una sola regla cubre todos los puertos del conjunto
        tcp dport @allowed_tcp accept

        # Todo lo demás cae por la política drop de la chain
    }

    chain forward {
        # Este servidor no enruta tráfico entre interfaces — drop explícito
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        # Permitir todo el tráfico saliente — ajustar si el contexto lo requiere
        type filter hook output priority 0; policy accept;
    }
}

# Tabla separada para NAT — solo IPv4/IPv6 con familias que lo soporten
# inet no soporta NAT, se necesita tabla ip explícita
table ip nat {

    chain prerouting {
        type nat hook prerouting priority -100;
        # Redirigir tráfico HTTP entrante al puerto 8080 de un servicio local
        # Caso típico: proxy inverso o contenedor sin privilegios
        tcp dport 80 redirect to :8080
    }

    chain postrouting {
        type nat hook postrouting priority 100;
        # MASQUERADE para interfaces que obtienen IP dinámica (ej: PPPoE, DHCP público)
        # Si la IP es estática, usar SNAT es más eficiente — MASQUERADE hace lookup en cada paquete
        oif "eth0" masquerade
    }
}
EOF

# Validar la sintaxis sin aplicar nada todavía
nft -c -f /etc/nftables.conf

# Aplicar de forma atómica — o entra todo o no entra nada
nft -f /etc/nftables.conf

# Verificar que el ruleset resultante es exactamente lo esperado
nft list ruleset

# Habilitar para que cargue en cada arranque
systemctl enable nftables
systemctl restart nftables

# Comprobar que el servicio cargó sin errores
systemctl status nftables --no-pager

Lo que está pasando en cada decisión

La línea flush ruleset al inicio del archivo no es un detalle cosmético. Es lo que convierte el archivo en una operación atómica real: cuando nft -f procesa el archivo completo, el kernel aplica todas las instrucciones como una transacción. Si cualquier línea falla, nada cambia. Esto contrasta directamente con el flujo de iptables donde cada iptables -A es una syscall independiente.

El uso de set allowed_tcp con type inet_service no es solo legibilidad. nftables implementa los conjuntos internamente como estructuras de datos eficientes (hash tables para conjuntos sin rangos, interval trees para rangos). Cuando tienes veinte puertos en el conjunto, la evaluación sigue siendo O(1) en lugar de recorrer veinte reglas secuencialmente como haría iptables.

La regla ct state invalid drop antes de procesar las reglas de aceptación es importante en producción. Los paquetes con estado invalid según el connection tracking son paquetes que no pertenecen a ninguna conexión conocida y no tienen las características de una conexión nueva válida. Dejarlos llegar al resto de las reglas es trabajo innecesario y puede ser un vector de evasión.

La decisión de usar table ip nat en lugar de table inet nat no es arbitraria: NAT con nftables requiere tablas de familia específica (ip para IPv4, ip6 para IPv6). La familia inet no soporta hooks de NAT. Si intentas definir NAT en inet, el comando falla con un error de prioridad.

En chain postrouting, el comentario sobre masquerade vs SNAT merece atención. masquerade hace una consulta a la tabla de routing en cada paquete para obtener la IP de salida actual, porque asume que puede cambiar. Con IP estática, snat to 203.0.113.1 elimina esa consulta y reduce overhead en enlaces de alto tráfico.

El flag -c en nft -c -f /etc/nftables.conf ejecuta una validación completa de sintaxis y semántica sin modificar el estado del kernel. Intégralo en cualquier pipeline de CI/CD que gestione configuración de firewall: es la diferencia entre descubrir un error de sintaxis antes del despliegue o después de dejarte fuera del servidor.

95

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio