
🌐 También en: English · Deutsch · Français
Actualización rápida del repo: el repositorio de GitHub www_k3s acaba de recibir una renovación a fondo del cortafuegos. La configuración antigua usaba el clásico iptables, que funcionaba bien, pero empezaba a parecerse a conducir un coche antiguo con estárter manual y doble embrague. Tocaba actualizar. El cortafuegos está ahora completamente reescrito en nftables, el sucesor moderno que viene por defecto en Linux desde el kernel 3.13. Aquí te cuento por qué es la decisión correcta y recorro a fondo lo que hace realmente el nuevo conjunto de reglas. 🧵
⚡ nftables vs. iptables: por qué importa
iptables es el filtro de paquetes estándar de Linux desde finales de los 90. Funciona, está más que probado y aproximadamente mil millones de respuestas de Stack Overflow hacen referencia a él. Pero se le nota la edad:
- Sin actualizaciones atómicas de reglas: los cambios se aplican línea a línea, así que hay una breve ventana en la que tu conjunto de reglas está aplicado solo a medias
- Herramientas separadas para IPv4/IPv6: iptables frente a ip6tables, dos almacenes de reglas distintos que hay que mantener sincronizados
- Sin sets nativos: las listas de bloqueo de IP requieren herramientas de terceros como ipset
- Rendimiento: la comparación de reglas es lineal; cuantas más reglas, más lento
nftables (introducido en Linux 3.13 y por defecto en la mayoría de las distribuciones desde ~2019) soluciona todo esto:
- ✅ Un único conjunto de reglas para IPv4 + IPv6 (
table inet) - ✅ Sustitución atómica de reglas: todo el conjunto se intercambia en una sola llamada al sistema
- ✅ Sets nativos con soporte de caducidad (listas de bloqueo de IP integradas, sin necesidad de ipset)
- ✅ Mejor rendimiento gracias a una máquina virtual compilada JIT en el kernel
En AlmaLinux 9, nftables es la opción por defecto del sistema. El comando iptables es literalmente solo una capa de compatibilidad sobre nftables. Así que usar la sintaxis real de iptables en 2025 significa escribir reglas de iptables que de todos modos se traducen a reglas de nftables; simplemente te pierdes toda la sintaxis bonita por el camino. 😅
📄 El conjunto de reglas, sección por sección
El cortafuegos es una plantilla Jinja2 de Ansible (roles/common_firewall/templates/fw.nft.j2) que se renderiza para cada host y se aplica mediante nftables. Vamos a recorrerla.
🗂️ Los sets de la lista de bloqueo
set banned4 {
type ipv4_addr
flags timeout
timeout 1d
}
set banned6 {
type ipv6_addr
flags timeout
timeout 1d
}Dos sets, uno para IPv4 y otro para IPv6, que hacen de lista de bloqueo dinámica. flags timeout + timeout 1d significa que cada entrada caduca automáticamente a las 24 horas. fail2ban escribe en estos sets cuando detecta actividad de fuerza bruta o escaneo de puertos. No hace falta ningún cron job para limpiar bloqueos antiguos. 🧹
🔗 Cadena INPUT: el plato fuerte
La política por defecto es DROP: todo lo que no esté permitido explícitamente se descarta en silencio.
iif lo acceptEl tráfico de loopback (127.0.0.1, ::1) se acepta siempre. Toda la comunicación interna del host (kubectl, kubelet, el scraping de Prometheus, los sockets de la base de datos) pasa por aquí.
ct state established,related accept
ct state invalid dropEl seguimiento de conexiones (connection tracking) hace el trabajo duro. Las conexiones ya establecidas se aceptan sin comprobar más reglas. Los paquetes en estado inválido (p. ej. paquetes TCP que no pertenecen a ninguna conexión conocida) se descartan de inmediato.
tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg drop # XMAS
tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 drop # NULLEl clásico descarte de paquetes malformados. Los paquetes XMAS tienen todos los flags TCP activados a la vez: imposible en tráfico real, lo usan los escáneres de puertos (nmap -sX). Los paquetes NULL no tienen ningún flag: lo mismo. Ambos son trucos de fingerprinting y evasión. ¡Fuera! 🎄🚫
ct state new tcp flags & (syn|ack) == syn|ack reject with tcp resetAntispoofing: un SYN,ACK que llega como conexión nueva significa que alguien está falsificando de la nada la respuesta a un handshake TCP. Se rechaza con un TCP RST.
tcp flags & (syn|ack|fin|rst) == rst limit rate 1/second accept
tcp flags & (syn|ack|fin|rst) == rst dropProtección contra inundaciones de RST. Los paquetes RST son legítimos (sirven para cerrar conexiones), pero una avalancha de ellos es una técnica de DoS. Se permite como máximo uno por segundo y el resto se descarta.
ip saddr @banned4 drop
ip6 saddr @banned6 dropComprobación de los sets dinámicos de bloqueo de fail2ban. Están colocados antes de las reglas de excepción de K3s para que una IP bloqueada nunca pueda colarse por las exenciones del CIDR de pods que hay más abajo.
{% if 'nextcloud' in group_names %}
ip saddr {{ k3s_pod_cidr }} tcp dport 3306 accept
ip saddr {{ k3s_pod_cidr }} tcp dport 6379 accept
{% endif %}Un condicional de Jinja2: este bloque solo aparece en los hosts de Nextcloud. En los servidores de Nextcloud, MariaDB y Redis funcionan como servicios del host (no dentro de K3s), así que los pods tienen que llegar a ellos a través de la IP del host. En los hosts de WordPress, MariaDB se ejecuta dentro del clúster como pod, por lo que estas reglas sobran y se omiten. 🧠
ip saddr {{ server_ipv4 }} tcp dport { 3000, 10254 } acceptingress-nginx se ejecuta con hostNetwork: true, lo que significa que vive en el espacio de nombres de red del host. Cuando reenvía una petición a Grafana (puerto 3000) mediante un ExternalService de Kubernetes, la IP de origen es la propia IP pública del host, no una IP de pod. Lo mismo ocurre con las sondas de salud del kubelet en el puerto 10254. Así que permitimos la IP propia del host, pero solo para estos dos puertos concretos, nada de un accept general. 🎯
ip saddr {
0.0.0.0/8, 10.0.0.x/8, 127.0.0.0/8, 169.254.0.0/16,
172.16.0.x/12, 192.168.0.x/16, 224.0.0.0/4, 240.0.0.0/5
} dropSe descartan todos los rangos de origen RFC-1918 y reservados que llegan por la interfaz externa. Si un paquete de internet afirma venir de 10.x.x.x o 192.168.x.x, está falsificado. Las excepciones del CIDR de pods de K3s de arriba están colocadas antes de este bloque precisamente para que el tráfico legítimo de los pods no quede atrapado aquí.
icmp type { destination-unreachable, echo-request, time-exceeded, parameter-problem } acceptSolo se permiten los tipos de ICMP que de verdad necesitamos: ping, traceroute y las señales de red inalcanzable. Todo lo demás (redirect, router-advertisement, etc.) se descarta.
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem,
router-advertisement, router-solicitation,
nd-neighbor-solicitation, nd-neighbor-advertisement,
echo-request, echo-reply } acceptICMPv6 necesita una lista de permitidos más larga porque IPv6 depende de él por completo. El Neighbor Discovery Protocol (NDP), el equivalente en IPv6 de ARP, funciona sobre ICMPv6. Sin estos tipos, IPv6 simplemente no funciona.
ip daddr {{ server_ipv4 }} tcp dport { 80, 443 } ct state new accept
ip daddr {{ server_ipv4 }} udp dport 443 ct state new accept
ip6 daddr {{ server_ipv6 }} tcp dport { 80, 443 } accept
ip6 daddr {{ server_ipv6 }} udp dport 443 acceptSe abren HTTP, HTTPS y UDP/443 (QUIC/HTTP3) para el servidor web, tanto en IPv4 como en IPv6. La regla UDP es la que hace posible HTTP/3: los navegadores lo descubren mediante Alt-Svc y luego cambian a él en las conexiones siguientes. 🚀
ip daddr {{ server_ipv4 }} tcp dport 10022 ct state new acceptSSH en un puerto no estándar. A estas alturas, el puerto 22 es puro ruido.
limit rate 2/minute log prefix "nftables DROP IN: " level warn flags allRegistro con límite de frecuencia de todo lo demás antes del descarte por la política. Así ves lo que se bloquea sin inundar el log del kernel. El prefijo facilita buscarlo con grep en journald.
🔀 Cadena FORWARD
K3s gestiona sus propias cadenas KUBE-* para el enrutamiento de servicios; esta cadena solo tiene que dejar pasar el tráfico pod a pod y pod a servicio dentro del CIDR de pods y bloquear todo lo demás. Aquí también se comprueban los sets de bloqueo, para que las IP bloqueadas no puedan aprovechar el tráfico reenviado.
📤 Cadena OUTPUT
El tráfico saliente también está restringido con un DROP por defecto. Se permite: DNS, NTP, HTTP/HTTPS para descargar paquetes y renovar certificados ACME, SMTP para el envío de correo, el servidor de API de K3s en el 6443 y QUIC saliente. Todo lo demás, incluidas las conexiones salientes aleatorias que podría intentar un malware, se descarta y se registra.
🏁 Conclusión
El conjunto de reglas de nftables hace mucho trabajo en relativamente pocas líneas. Un solo archivo, doble pila IPv4/IPv6, listas de bloqueo dinámicas nativas, seguimiento de conexiones y excepciones pensadas para Kubernetes, todo en una unidad atómica que se aplica limpiamente en cada ejecución de Ansible. Es justo el tipo de cosa que te hace valorar la infraestructura como código. 🤓
El repo está en github.com/aptupgrademe/www_k3s por si quieres profundizar.




