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).
Importanteusa un único gestor de firewall. Si vas a gestionar iptables directamente, desactiva UFW; si prefieres algo más sencillo, UFW genera estas mismas reglas por ti. Docker también inserta sus propias reglas en iptables: si el servidor ejecuta Docker, no borres las cadenas
DOCKER*ni hagasiptables -Fa ciegas.
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:
| Comando | Efecto |
|---|---|
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 3 | Borra la regla número 3 |
iptables -D INPUT ... | Borra la regla que coincida exactamente con la especificación |
iptables -P INPUT DROP | Cambia 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
DROPenINPUTyFORWARDdescartan todo lo que no se permita explícitamente.OUTPUTqueda enACCEPTpara que el servidor pueda actualizarse y hacer consultas DNS sin reglas adicionales. - La regla
ESTABLISHED,RELATEDes 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
recentguarda 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
LOGva al final, justo antes de que actúe la políticaDROP, 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.v4yrules.v6en un repositorio o desplegarlos con Ansible en todos tus servidores.
