iptables es la herramienta clásica para definir las reglas del firewall del kernel Linux (Netfilter). Aunque en Ubuntu 24.04 funciona internamente sobre nftables (backend iptables-nft), su sintaxis sigue siendo la más documentada y la que usan muchos scripts y herramientas. En este tutorial crearás un firewall de entrada para un servidor web con SSH, lo aplicarás de forma segura con reversión automática, añadirás IPv6 y harás las reglas persistentes tras reiniciar.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Acceso a la consola del servidor (VNC o consola web del proveedor) por si una regla te deja sin SSH.
  • Saber qué puertos debe ofrecer el servidor. En este ejemplo: SSH (22), HTTP (80) y HTTPS (443).

Paso 1: Preparar el sistema

Comprueba la versión y el backend de iptables:

sudo iptables -V
iptables v1.8.10 (nf_tables)

(nf_tables) confirma que las reglas se guardan en nftables, algo transparente para ti mientras uses solo comandos iptables.

Si UFW está activo, desactívalo para que no compita con tus reglas:

sudo ufw status
sudo ufw disable

Instala iptables-persistent, que carga las reglas guardadas en cada arranque. Durante la instalación preguntará si quieres guardar las reglas actuales; responde Sí para IPv4 e IPv6, así se crean los archivos de reglas:

sudo apt update
sudo apt install iptables-persistent

Comprueba que existen los archivos:

ls /etc/iptables/
rules.v4  rules.v6

Paso 2: Conocer los comandos básicos

Las reglas se organizan en tablas (filter para filtrar, nat para traducir direcciones) y cadenas. En la tabla filter las cadenas principales son INPUT (tráfico hacia el servidor), OUTPUT (tráfico que sale de él) y FORWARD (tráfico que atraviesa el servidor, solo si hace de router). Cada paquete recorre las reglas de su cadena en orden hasta que una coincide; si ninguna coincide, se aplica la política de la cadena.

Lista las reglas actuales con contadores y número de línea:

sudo iptables -L INPUT -n -v --line-numbers
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

La forma más útil de ver las reglas es en el mismo formato con el que se escriben:

sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

Las operaciones que usarás a diario son:

ComandoEfecto
iptables -A INPUT ...Añade una regla al final de la cadena
iptables -I INPUT 3 ...Inserta una regla en la posición 3
iptables -D INPUT 3Borra la regla número 3
iptables -D INPUT ...Borra la regla que coincida exactamente con la especificación
iptables -P INPUT DROPCambia la política de la cadena

Los cambios hechos con estos comandos son inmediatos pero se pierden al reiniciar. En lugar de construir el firewall regla a regla en caliente, en este tutorial lo escribirás en un archivo y lo aplicarás de una vez, que es más seguro y fácil de revisar.

Paso 3: Escribir las reglas IPv4

Abre el archivo de reglas IPv4:

sudo nano /etc/iptables/rules.v4

Sustituye su contenido por lo siguiente:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Tráfico local y respuestas a conexiones ya establecidas
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP

# Ping, con límite para no convertirlo en vector de abuso
-A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/second --limit-burst 10 -j ACCEPT

# SSH: máximo 10 conexiones nuevas por minuto y por IP
-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --name ssh --set
-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --name ssh --update --seconds 60 --hitcount 10 -j DROP
-A INPUT -p tcp --dport 22 -j ACCEPT

# Web
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# Registrar lo que se descarta, como máximo 5 líneas por minuto
-A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-drop: " --log-level 4
COMMIT

Qué hace cada bloque:

  • Las políticas DROP en INPUT y FORWARD descartan todo lo que no se permita explícitamente. OUTPUT queda en ACCEPT para que el servidor pueda actualizarse y hacer consultas DNS sin reglas adicionales.
  • La regla ESTABLISHED,RELATED es la más importante: deja pasar las respuestas a conexiones que el propio servidor inició y los mensajes ICMP de error asociados. Sin ella, nada funcionaría.
  • El módulo recent guarda las IP que abren conexiones SSH y descarta a las que abren más de 10 en 60 segundos, lo que frena ataques de fuerza bruta sin afectar a un uso normal.
  • La regla LOG va al final, justo antes de que actúe la política DROP, así que registra exactamente lo que se descarta.

Si tu servidor escucha SSH en otro puerto, cámbialo en las tres reglas de SSH. Si ofrece otros servicios, añade reglas ACCEPT antes de la línea LOG. Para permitir un servicio solo desde una IP concreta, por ejemplo una base de datos:

-A INPUT -p tcp -s 198.51.100.20 --dport 5432 -j ACCEPT

Paso 4: Aplicar las reglas sin quedarte fuera

Comprueba primero que la sintaxis es válida. -t solo prueba el archivo, no lo aplica:

sudo iptables-restore -t /etc/iptables/rules.v4

Si no muestra nada, el archivo es correcto. Ahora aplícalo con iptables-apply, que carga las reglas y te pide confirmación; si no respondes en el tiempo indicado (porque has perdido la conexión), restaura automáticamente las reglas anteriores:

sudo iptables-apply -t 60 /etc/iptables/rules.v4
Applying new ruleset... done.
Can you establish NEW connections to the machine? (y/N)

Antes de contestar, abre una segunda sesión SSH al servidor desde otra terminal. Si entra sin problemas, responde y en la primera. Si no puedes conectar, no respondas: en 60 segundos las reglas se revierten solas.

Verifica las reglas cargadas y observa cómo suben los contadores:

sudo iptables -L INPUT -n -v --line-numbers
Chain INPUT (policy DROP 12 packets, 624 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1       40  3120 ACCEPT     0    --  lo     *       0.0.0.0/0            0.0.0.0/0
2     1822  152K ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
3        0     0 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID
...

Paso 5: Añadir las reglas IPv6

Si el servidor tiene IPv6, necesita su propio conjunto de reglas, que se gestiona con ip6tables. Dejarlo sin reglas equivale a dejar el servidor abierto por IPv6. Abre el archivo:

sudo nano /etc/iptables/rules.v6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# ICMPv6 es imprescindible: sin él fallan la resolución de vecinos y el descubrimiento de rutas
-A INPUT -p ipv6-icmp -j ACCEPT

-A INPUT -m conntrack --ctstate INVALID -j DROP

-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --name ssh6 --set
-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --name ssh6 --update --seconds 60 --hitcount 10 -j DROP
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

-A INPUT -m limit --limit 5/min -j LOG --log-prefix "ip6tables-drop: " --log-level 4
COMMIT

A diferencia de IPv4, en IPv6 no puedes bloquear ICMP en general: el protocolo lo usa para encontrar vecinos en la red local, y sin él el servidor perdería la conectividad IPv6. Por eso la regla de ICMPv6 va antes del descarte de paquetes INVALID.

Valida y aplica. Como la sesión SSH actual va normalmente por IPv4, basta con probar y cargar el archivo:

sudo ip6tables-restore -t /etc/iptables/rules.v6
sudo ip6tables-restore < /etc/iptables/rules.v6

Comprueba desde otra máquina con IPv6 que el servidor sigue respondiendo:

ping -6 -c 3 your_server_ipv6

Paso 6: Hacer las reglas persistentes

Como has editado directamente /etc/iptables/rules.v4 y rules.v6, esos archivos ya contienen tus reglas. El servicio netfilter-persistent los cargará en cada arranque. Asegúrate de que está habilitado:

sudo systemctl enable netfilter-persistent

Si en el futuro añades reglas en caliente con iptables -A o -I y quieres conservarlas, guárdalas en los mismos archivos con:

sudo netfilter-persistent save

Reinicia el servidor y verifica que las reglas siguen cargadas:

sudo reboot
sudo iptables -S INPUT | head -n 5
-P INPUT DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m limit --limit 5/sec --limit-burst 10 -j ACCEPT

Paso 7: Consultar el registro de paquetes descartados

La regla LOG envía cada paquete descartado al registro del kernel con el prefijo que definiste. Consúltalo con:

sudo journalctl -k -g 'iptables-drop' -n 20 --no-pager
sep 24 10:31:02 server kernel: iptables-drop: IN=eth0 OUT= SRC=192.0.2.77 DST=203.0.113.10 PROTO=TCP SPT=51234 DPT=3306 ...

SRC es la IP de origen y DPT el puerto al que intentaba conectar. Si ves tráfico legítimo descartado, añade la regla ACCEPT correspondiente.

Paso 8 (opcional): Reenviar un puerto con NAT

Si el servidor hace de puerta de enlace para una red privada, por ejemplo un contenedor o una VM en 10.0.0.20, puedes publicar uno de sus puertos. Primero habilita el reenvío de paquetes en el kernel de forma persistente:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ip-forward.conf
sudo sysctl --system

Añade al principio de /etc/iptables/rules.v4, antes de *filter, una sección para la tabla nat. Sustituye eth0 por tu interfaz pública:

*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.20:80
-A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
COMMIT

Como la política de FORWARD es DROP, permite ese tráfico en la sección *filter:

-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -i eth0 -p tcp -d 10.0.0.20 --dport 80 -m conntrack --ctstate NEW -j ACCEPT
-A FORWARD -s 10.0.0.0/24 -o eth0 -j ACCEPT

Vuelve a aplicar con sudo iptables-apply -t 60 /etc/iptables/rules.v4 y prueba curl -I http://your_server_ip:8080 desde fuera.

Solución de problemas

Te has quedado sin SSH: entra por la consola del proveedor y ejecuta sudo iptables -P INPUT ACCEPT seguido de sudo iptables -F INPUT para abrir temporalmente la cadena. Corrige el archivo y vuelve a aplicarlo con iptables-apply.

iptables-restore: line N failed: hay un error de sintaxis en esa línea del archivo. Los fallos más comunes son una opción mal escrita o que falte COMMIT al final de una tabla.

El servidor no resuelve nombres ni descarga actualizaciones: falta la regla ESTABLISHED,RELATED en INPUT o está después de una regla DROP. El orden de las reglas importa.

Las reglas desaparecen tras reiniciar: comprueba que netfilter-persistent está habilitado y que UFW sigue desactivado (sudo systemctl is-enabled ufw); si UFW arranca, reemplaza tus reglas.

Conclusión

Has construido un firewall con política de descarte por defecto, límite de conexiones SSH, registro de paquetes descartados y reglas equivalentes para IPv6, aplicado con reversión automática y persistente tras reiniciar. Mantener las reglas en /etc/iptables/rules.v4 y rules.v6 te da un único sitio que revisar y versionar.

Como siguientes pasos puedes:

  • Añadir Fail2ban para bloquear durante un tiempo las IP que fallan repetidamente al autenticarse.
  • Migrar a nftables, el sucesor de iptables, que usa una sola sintaxis para IPv4 e IPv6.
  • Guardar rules.v4 y rules.v6 en un repositorio o desplegarlos con Ansible en todos tus servidores.