¿Te ha pasado alguna vez? Dejas tu servidor tranquilo, te vas a dormir, y a la mañana siguiente revisas los logs de SSH y te encuentras con esto:
Jun 17 03:14:22 servidor sshd[12345]: Failed password for root from 103.235.46.92 port 59231 ssh2
Jun 17 03:14:24 servidor sshd[12345]: Failed password for root from 103.235.46.92 port 59231 ssh2
Jun 17 03:14:26 servidor sshd[12345]: Failed password for root from 103.235.46.92 port 59231 ssh2
Y así 500 veces más. Alguien desde una IP en la otra punta del mundo está probando contraseñas contra tu servidor. No importa que hayas desactivado el login por contraseña en el capítulo 1, o que hayas puesto nftables con default-drop en el capítulo 2. El puerto SSH está abierto porque tú necesitas entrar. Y mientras esté abierto, van a llamar a la puerta.
fail2ban es el portero que se cansa de abrir la puerta al mismo pesado y le dice: vale, ya está bien, no vuelvas en una hora.
¿Qué es fail2ban y para qué sirve?
Te lo voy a poner fácil. fail2ban es un demonio que lee logs de aplicaciones, detecta patrones de intentos fallidos y ejecuta acciones cuando se supera un umbral. Dicho así suena muy serio. Dicho en cristiano: vigila quién llama a la puerta, y si alguien llama demasiadas veces sin identificarse correctamente, le cierra la puerta en la cara durante un tiempo.
Funciona con tres componentes básicos,
- Filtros. Expresiones regulares que definen qué buscar en los logs. Una línea como Failed password for root casa con el filtro de SSH, por ejemplo.
- Acciones. Qué hacer cuando se detecta un abuso. Por defecto, añadir una regla en el firewall para bloquear la IP. También puede enviar un correo, ejecutar un script, o lo que se te ocurra.
- Jails. La combinación de un filtro + una acción + unos parámetros de tiempo. Cada cárcel protege un servicio concreto.
Imagina un bar. El filtro es el portero que observa a la gente que intenta entrar. La acción es la mano del portero impidiendo el paso. La jail es la instrucción: si alguien intenta colarse tres veces en cinco minutos, no le dejas entrar durante una hora.
La gracia de fail2ban es que es reactivo, pero no necesita intervención humana. Detecta, decide y actúa solo. Tú configuras las reglas y él se encarga del resto.
Diferencia con nftables: preventivo vs reactivo
Aquí tengo que pararme un momento porque es importante entenderlo bien.
nftables, que configuramos en el capítulo anterior, es preventivo. Tú defines unas reglas que se aplican siempre, pase lo que pase. El puerto 22 solo acepta conexiones desde la VPN. El puerto 443 está abierto para todo el mundo. Por defecto, deniega todo lo que no esté explícitamente permitido. Es como tener una puerta blindada: impide la entrada, pero no distingue entre quien llama una vez y quien llama mil veces.
fail2ban es reactivo. No actúa hasta que detecta un comportamiento anómalo. Primero observa, luego decide. Es como tener un portero que al principio deja pasar a todo el mundo, pero si ve que alguien se comporta de forma sospechosa, le dice oye, tu nombre no está en la lista, fuera de aquí.
¿Significa eso que fail2ban sustituye a nftables? No. Ni de coña. Son complementarios.
nftables es la puerta. fail2ban es el portero. La puerta está ahí siempre, cerrada para lo que no toca, abierta para lo que sí. El portero observa, aprende y reacciona cuando alguien insiste donde no debe.
Te cuento una anécdota. Cuando monté mi primer servidor hace años, pensaba que con un firewall bien configurado ya estaba todo hecho. Y sí, el firewall paraba lo burdo. Pero los logs de SSH eran una auténtica locura. Cientos de intentos cada noche. El firewall no distingue entre una conexión legítima y un bot probando contraseñas, solo mira puertos e IPs. fail2ban fue la pieza que me faltaba. Desde entonces, no concibo un servidor sin él.
La clave está en que fail2ban modifica las reglas de nftables cuando detecta un abuso. Añade una regla temporal que deniega todo el tráfico desde la IP infractora. Cuando pasa el tiempo de baneo, elimina la regla. El baneo no es permanente, es un timeout.
Esto es importante: fail2ban no tiene su propio firewall. Usa el que ya tienes. Ya sea iptables, nftables, pf en BSD, o incluso ipset. Tú le dices qué backend usar, y él se encarga de añadir y quitar reglas.
En nuestro caso, como ya tenemos nftables configurado del capítulo anterior, vamos a integrar fail2ban para que use nftables como backend.
Instalación
En Ubuntu 24.04, fail2ban está en los repositorios oficiales. Nada de PPAs ni historias raras.
sudo apt update
sudo apt install fail2ban
Así de simple. Con esto tienes fail2ban instalado. Pero no está activo todavía. Y no te recomiendo que lo actives sin más, porque la configuración por defecto tiene algunas cosillas que deberías ajustar.
Vamos a ver qué nos ha dejado el paquete,
/etc/fail2ban/fail2ban.conf: configuración general del demonio (logs, socket, PID)/etc/fail2ban/jail.conf: definiciones de todas las jails (no tocar, se sobreescribe con actualizaciones)/etc/fail2ban/jail.d/: directorio para configuraciones personalizadas/etc/fail2ban/filter.d/: los filtros (expresiones regulares para cada servicio)/etc/fail2ban/action.d/: las acciones (nftables, iptables, email, etc.)
La regla de oro con fail2ban: no toques los archivos originales. Crea tus propios archivos .local o en jail.d/. Cuando actualices el paquete, tus cambios no se perderán. Es la misma filosofía que con SSH: nunca edites el archivo de configuración que viene por defecto, crea uno propio que sobreescriba lo que necesites.
Estructura de configuración
Vamos a ver cómo se organiza todo esto.
jail.conf vs jail.local
El archivo principal es /etc/fail2ban/jail.conf. Si lo abres, verás una configuración completa con todas las jails disponibles. Pero no debes editarlo. Cada vez que actualices fail2ban, ese archivo se sobreescribe y perderías tus cambios.
La solución es crear /etc/fail2ban/jail.local. fail2ban lee primero jail.conf y luego jail.local. Lo que definas en jail.local sobreescribe lo que viene en jail.conf. Es exactamente el mismo mecanismo que con los archivos de configuración de SSH, por cierto.
Puedes poner toda tu configuración en jail.local o, mejor aún, crear archivos separados en /etc/fail2ban/jail.d/. Por ejemplo, jail.d/sshd.conf, jail.d/traefik-auth.conf, etc. Es más ordenado y fácil de mantener.
¿Y qué pones dentro? Pues básicamente dos tipos de cosas:
- Configuración global: valores por defecto que aplican a todas las jails
- Definiciones de jails: configuración específica para cada servicio
Filtros
Los filtros están en /etc/fail2ban/filter.d/. Cada filtro es un archivo con una expresión regular que busca en los logs las líneas que indican un intento fallido.
Por ejemplo, el filtro de SSH (filter.d/sshd.conf) busca líneas como:
Failed password for root from 192.168.1.1 port 22 ssh2
Y extrae la IP del infractor. Hay filtros para SSH, Nginx, Apache, Postfix, Dovecot, ProFTPD, y un montón más. Si necesitas algo muy específico, puedes crear tu propio filtro.
Acciones
Las acciones están en /etc/fail2ban/action.d/. Son scripts que se ejecutan cuando se supera el umbral. Mi recomendación es usar nftables-multiport si tienes nftables, que es tu caso después del capítulo 2.
Cada acción tiene tres fases,
- actionban: se ejecuta cuando se banea una IP. Añade la regla en el firewall.
- actionunban: se ejecuta cuando expira el baneo. Elimina la regla.
- actioncheck: verifica que las reglas están en orden.
El sistema es flexible. Puedes tener acciones que manden un correo, que escriban en un log, que ejecuten un webhook, o todo a la vez.
Configuración básica
Vamos al lío. Crea el archivo de configuración:
sudo nano /etc/fail2ban/jail.local
Y dentro ponemos esto:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 10.0.0.0/8
bantime = 3600
findtime = 600
maxretry = 5
banaction = nftables-multiport
backend = systemd
Vamos por partes,
- ignoreip: IPs que nunca se banean. Aquí pones tu propia red local, tu VPN, tu IP pública si es fija. Lo último que quieres es quedarte fuera de tu propio servidor. Créeme, he visto a más de uno banearse a sí mismo y tener que entrar por la consola del VPS para desbloquearse. No seas ese.
- bantime: tiempo en segundos que dura el baneo. 3600 segundos = 1 hora. Puedes poner 86400 (24h), 604800 (7 días), o lo que te parezca. Más adelante veremos una jail especial para reincidentes con baneo permanente.
- findtime: ventana de tiempo en segundos en la que se cuentan los intentos. Si pones 600 (10 minutos), y alguien hace 5 intentos en menos de 10 minutos, salta el baneo.
- maxretry: número máximo de intentos antes del baneo. Con 5 y findtime de 600, alguien puede intentar 5 veces en 10 minutos. Al sexto intento, baneado.
- banaction: qué acción ejecutar al banear. Ponemos
nftables-multiportpara que use nftables, que es lo que tienes del capítulo 2. - backend: cómo lee fail2ban los logs.
systemdusa journalctl. En Ubuntu 24.04 con systemd, esta es la opción correcta para SSH porque los logs van al journal.
La jail de SSH
Con la configuración global ya definida, vamos a activar la jail de SSH:
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
backend = %(sshd_backend)s
Pero espera, si has cambiado el puerto de SSH (como vimos en el capítulo 1), tendrás que poner el puerto correcto. En mi caso, que lo cambié al 8822:
[sshd]
enabled = true
port = 8822
logpath = %(sshd_log)s
backend = %(sshd_backend)s
Fíjate en las variables %(sshd_log)s y %(sshd_backend)s. Están definidas en jail.conf y apuntan a los valores por defecto. Como no las estamos sobreescribiendo, fail2ban usa las del archivo original. Es una forma limpia de no repetir configuraciones.
Puedes sobreescribir cualquiera de los valores globales dentro de la jail. Por ejemplo, si quieres que SSH tenga un maxretry más bajo:
[sshd]
enabled = true
port = 8822
maxretry = 3
logpath = %(sshd_log)s
backend = %(sshd_backend)s
Aquí maxretry = 3 sobreescribe el valor global de 5, solo para esta jail. Personalmente, para SSH me gusta ser más restrictivo: 3 intentos y fuera.
La jail de Traefik
Si usas Traefik como proxy inverso (que es lo que tienes si has seguido mis tutoriales en atareao.es), estás igual de expuesto que con Nginx o Apache. Bots escaneando rutas, gente probando credenciales en tus servicios autenticados, scripts buscando vulnerabilidades… fail2ban puede echarlos también.
Lo bueno es que fail2ban ya incluye filtros oficiales para Traefik desde hace tiempo. No necesitas instalar nada extra ni crear filtros a mano. Vienen incluidos en el paquete.
Configurar Traefik para generar logs de acceso
Antes de nada, Traefik tiene que estar escribiendo logs de acceso. Si no los tiene activados, fail2ban no tiene de dónde leer. En tu archivo de configuración de Traefik (normalmente traefik.yml), tienes que activar el logging de acceso:
accessLog:
filePath: /var/log/traefik/access.log
format: common
El formato common usa el mismo formato que Nginx, y es el que esperan los filtros de fail2ban. Si usas formato json, tendrías que adaptar los filtros.
Si Traefik corre en Docker o Podman, asegúrate de montar el volumen para que el log sea accesible desde el host:
volumes:
- /var/log/traefik:/var/log/traefik
Filtro traefik-auth
El filtro traefik-auth detecta errores 401: peticiones a servicios que requieren autenticación y el cliente no tiene credenciales válidas. Esto cubre cualquier servicio detrás de Traefik que tenga un login: Authelia, Authentik, Grafana, o el propio dashboard de Traefik si lo tienes autenticado.
[traefik-auth]
enabled = true
port = http,https
filter = traefik-auth
logpath = /var/log/traefik/access.log
maxretry = 5
bantime = 3600
¿Qué consigues con esto? Si alguien prueba contraseñas en tu dashboard de Traefik, en Authelia, o en cualquier servicio autenticado que pase por Traefik, tras 5 intentos fallidos en 10 minutos, su IP se banea automáticamente.
Filtro traefik-botsearch
El filtro traefik-botsearch detecta errores 404 repetidos. Los bots maliciosos suelen escanear rutas conocidas en busca de vulnerabilidades: /wp-admin, /.env, /admin, /backup, /phpmyadmin, etc. Cada 404 es una prueba, y si alguien prueba demasiadas, es que no es un usuario legítimo.
[traefik-botsearch]
enabled = true
port = http,https
filter = traefik-botsearch
logpath = /var/log/traefik/access.log
maxretry = 10
bantime = 3600
Con maxretry = 10, cualquiera que genere 10 errores 404 se lleva un baneo de una hora. Esto cubre tanto a los bots que escanean rutas como a scripts mal configurados.
Filtro personalizado para 429 (too many requests)
Si tienes configurado rate limiting en Traefik (lo veremos más adelante en la serie), los clientes que superen el límite recibirán un 429. Puedes crear un filtro personalizado para banear a quienes abusen:
Crea /etc/fail2ban/filter.d/traefik-ratelimit.conf:
[Definition]
failregex = ^<HOST> - - \[.*\] ".*" 429 .*$
ignoreregex =
Y la jail correspondiente:
[traefik-ratelimit]
enabled = true
port = http,https
filter = traefik-ratelimit
logpath = /var/log/traefik/access.log
maxretry = 3
bantime = 1800
Este es más agresivo: 3 veces con 429 y te banean media hora. Normalmente un 429 ya es el rate limiting de Traefik diciendo tranquilo, ve más despacio. Si alguien lo ignora y sigue insistiendo, fail2ban le corta el paso.
Integración con nftables
Aquí viene la parte jugosa. En el capítulo 2 configuramos nftables con una tabla inet filter y reglas bien definidas. Ahora tenemos que hacer que fail2ban añada reglas a esa misma tabla.
El paquete fail2ban en Ubuntu 24.04 ya viene con la acción nftables-multiport en /etc/fail2ban/action.d/nftables-multiport.conf. Pero por defecto usa el nombre de tabla filter, que puede chocar con tu configuración si tienes una tabla con otro nombre.
Vamos a ver qué contiene,
sudo cat /etc/fail2ban/action.d/nftables-multiport.conf
Busca la línea que define la tabla:
table = filter
Si en tu configuración del capítulo 2 usaste otro nombre (por ejemplo, inet-filter), tienes que decírselo a fail2ban. Puedes hacerlo en el jail.local:
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true port = 8822 maxretry = 3 logpath = %(sshd_log)s backend = %(sshd_backend)s nftables_family = inet nftables_table = filter chain = input
Los parámetros específicos de nftables:
- nftables_family: la familia de direcciones (
inetpara IPv4+IPv6,ipsolo IPv4,ip6solo IPv6) - nftables_table: el nombre de tu tabla nftables. Por defecto
filter. - chain: la cadena donde añadir las reglas de bloqueo. Normalmente
input.
Si tienes curiosidad de cómo funciona por dentro, la acción nftables-multiport hace algo como esto cuando banea:
nft add rule inet filter input ip saddr <IP> counter drop
Y cuando desbanea:
nft delete rule inet filter input handle <handle>
La gracia está en que fail2ban gestiona los handles automáticamente. No tienes que preocuparte de limpiar reglas muertas ni nada de eso. fail2ban lleva la cuenta.
Si prefieres que las IPs baneadas vayan a una cadena separada (más ordenado), puedes configurarlo así:
[sshd]
enabled = true
port = 8822
maxretry = 3
logpath = %(sshd_log)s
backend = %(sshd_backend)s
nftables_family = inet
nftables_table = filter
chain = input
nftables_chain = f2b-sshd
Esto crea una cadena f2b-sshd dentro de tu tabla inet filter, y añade una regla en la cadena input que salta a f2b-sshd. Así las reglas de fail2ban están todas juntas y no se mezclan con las tuyas.
Para que esto funcione, fail2ban necesita que la cadena f2b-sshd exista en tu configuración de nftables. Si no existe, fail2ban la crea automáticamente al arrancar. Pero si tu configuración de nftables se recarga (con nft -f), la cadena desaparece y fail2ban tiene que recrearla.
Mi recomendación: define la cadena explícitamente en tu archivo de nftables, aunque fail2ban la cree sola. Así evitas sorpresas si recargas las reglas.
La jail recidive: para reincidentes
Hay un tipo de atacante especialmente pesado: el que ya ha sido baneado, espera a que pase el baneo, y vuelve a intentarlo. Un bot, básicamente. Para estos casos tenemos la jail recidive.
La idea es sencilla: si alguien ha sido baneado ya por otras jails y vuelve a ser detectado, se le aplica un baneo mucho más largo. O permanente.
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = nftables-allports
bantime = 604800
findtime = 86400
maxretry = 3
Fíjate en lo que hace:
- logpath: lee el propio log de fail2ban. No busca en logs de servicios, busca en quién ha sido baneado ya.
- findtime: 86400 segundos = 24 horas. Mira los últimos baneos en el último día.
- maxretry: si alguien ha sido baneado 3 veces en 24 horas por cualquier jail, salta esta.
- bantime: 604800 segundos = 7 días.
- banaction:
nftables-allports. Bloquea todos los puertos, no solo el del servicio atacado.
El recidive es un cortafuegos dentro del cortafuegos. La primera línea de defensa (SSH, Traefik) detecta el ataque y banea por una hora. Si el atacante es persistente y vuelve a intentarlo varias veces en un día, recidive le cierra la puerta por una semana y en todos los puertos.
Si quieres ser más drástico, puedes poner bantime = -1 para un baneo permanente. Pero piensa bien antes de hacerlo. Las IPs públicas se reasignan, y podrías estar baneando para siempre a un pobre inocente que heredó una IP comprometida. Yo prefiero 7 días y luego olvidar. Si vuelve, otros 7.
Verificación
Una vez que tienes todo configurado, toca verificar que funciona. Vamos paso a paso.
Estado del servicio
Lo primero, comprobar que fail2ban está corriendo:
sudo systemctl status fail2ban
Si no está activo, lo arrancamos y lo habilitamos:
sudo systemctl enable fail2ban --now
Listar jails activas
sudo fail2ban-client status
Te muestra un listado de las jails activas. Si has configurado sshd, traefik-auth y traefik-botsearch, verás algo como:
Status
|- Number of jail: 3
`- Jail list: sshd, traefik-auth, traefik-botsearch
Estado de una jail concreta
sudo fail2ban-client status sshd
Esto te da información detallada: número de fallos actuales en el findtime, IPs actualmente baneadas, total de baneos desde que arrancó la jail.
Log de fail2ban
sudo journalctl -u fail2ban -n 50 --no-pager
O si tienes el log en archivo:
sudo tail -f /var/log/fail2ban.log
Aquí ves en tiempo real qué está haciendo fail2ban: qué líneas ha matchado, qué IPs ha baneado, cuándo expiran los baneos.
Probar con fail2ban-regex
Esta herramienta es muy útil para probar filtros sin tener que esperar a un ataque real. Puedes pasarle un filtro y un archivo de log y te dice si las líneas casan o no.
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
Te muestra cuántas líneas del log han casado con el filtro, cuántas IPs ha encontrado, y qué líneas no han casado (para que puedas ajustar el filtro).
Si quieres probar con una línea concreta:
sudo fail2ban-regex "Failed password for root from 192.168.1.100 port 22 ssh2" /etc/fail2ban/filter.d/sshd.conf
Te dice si esa línea casa con el filtro y qué información extrae de ella.
Simular un baneo manual
Para probar que la acción de nftables funciona, puedes banear una IP manualmente:
sudo fail2ban-client set sshd banip 192.168.1.100
Comprueba que la regla se ha añadido a nftables:
sudo nft list ruleset | grep 192.168.1.100
Y desbanea:
sudo fail2ban-client set sshd unbanip 192.168.1.100
Comprobar las reglas nftables
sudo nft list chain inet filter input
Si has configurado cadenas separadas para fail2ban:
sudo nft list chain inet filter f2b-sshd
Deberías ver las reglas de bloqueo de fail2ban, cada una con su contador de paquetes.
Buenas prácticas
Termino con una lista de cosillas que he aprendido con los años usando fail2ban en distintos servidores,
- No te banee a ti mismo. Configura bien
ignoreipcon tus rangos de confianza. Si tienes IP dinámica en casa, usa una VPN y pon el rango de la VPN en ignoreip. O usa un dyndns y un script que actualice el ignoreip. Y esto es una faena: he estado al otro lado, teniendo que pedir a un amigo que entre por la consola del VPS porque me había baneado a mí mismo. No lo recomiendo. - Baneos razonables. No pongas bantime de 30 días en la jail normal. Usa recidive para los persistentes. La gente comete errores, las herramientas de CI/CD fallan, y tú puedes equivocarte de contraseña un par de veces seguidas.
- Monitoriza, no ignores. fail2ban no es configurar y olvidar. Mira los logs de vez en cuando. Ajusta los filtros si ves falsos positivos. Añade nuevas jails si pones nuevos servicios.
- No actives todas las jails. Cada jail tiene un coste: consume CPU leyendo logs y evaluando expresiones regulares. Activa solo las que necesites.
- Cuidado con los contenedores. Si usas Docker o Podman, los logs de las aplicaciones no siempre van al journal del host. Tendrás que configurar fail2ban para que lea los logs de los contenedores, o usar volúmenes compartidos.
- Prueba antes de desplegar. Usa
fail2ban-regexpara verificar que tus filtros casan con las líneas reales de tus logs. No te fíes de lo que pone en la documentación, mira lo que genera tu aplicación. - fail2ban no es un WAF. No intentes proteger una aplicación web compleja solo con fail2ban. Usa un WAF (Web Application Firewall) como ModSecurity o Coraza para eso. fail2ban es para lo burdo: escáneres, bots, ataques de fuerza bruta.
Conclusión
fail2ban es una pieza más en tu rompecabezas de seguridad. No es la pieza principal, pero es importante. Junto con el blindaje SSH del capítulo 1 y nftables del capítulo 2, forma la base de seguridad de cualquier servidor auto-alojado.
La próxima vez que mires los logs y veas 500 intentos de SSH en una noche, sabrás que fail2ban está ahí, echando a los pesados uno tras otro, sin que tengas que mover un dedo.
En el próximo capítulo vamos a rematar el bloque de blindaje básico con algo fundamental: crear un usuario no-root, endurecer sudo, y proteger los logs con auditoría básica. Porque no basta con tener seguridad, hay que asegurarse de que quien tiene acceso lo tiene con el mínimo privilegio posible.
@atareao También hay sshguard https://www.sshguard.net/ – más ligero y más fácil de configurar que fail2ban. Pero claro, fail2ban tiene más funciones.
@Haydar muchas gracias. No conocía sshguard, siempre he confiado en fail2ban y crowdsec. Lo anotoDándole una vuelta y pensando en el consumo de recursos, he caído en su no habría versión de fail2ban en rust. Parece que si que hay una. Voy a echarme un ojo 👀