Fail2Ban lee los logs de los servicios y, tras varios inicios de sesión fallidos desde una misma dirección IP, añade esa dirección al firewall durante un tiempo determinado. CSF hace lo mismo con su demonio LFD y además gestiona las reglas de iptables. Ambos bloquean una IP concreta a nivel de red, así que no detienen la fuerza bruta repartida entre miles de direcciones, no ven lo que nunca llega a un log y, detrás de Cloudflare, bloquean la dirección equivocada si no se configura algo más. A continuación tienes jails funcionales para SSH, nginx y WordPress, recidive y seis trampas que reprodujimos en un banco de pruebas.
Cómo lo probamos
El 10 de noviembre de 2026 en un banco de pruebas: un contenedor con Ubuntu 24.04, Fail2Ban 1.0.2 del paquete de Ubuntu, nftables, OpenSSH 9.6p1, nginx 1.24.0, PHP-FPM 8.3, MariaDB 10.11 y WordPress 6.4.3. El servidor, el atacante, el administrador y un proxy inverso funcionaban cada uno en su propio network namespace; sshpass y curl generaban los intentos de contraseña, y el tiempo hasta el bloqueo se midió en fail2ban.log con marcas de tiempo en milisegundos. IPv6 se probó en un kernel User-Mode Linux 6.17 aparte, porque IPv6 está desactivado en el kernel del contenedor. CSF no se instaló en el banco: la parte sobre CSF se basa en su readme y su csf.conf del código abierto. Documentación de Fail2Ban, CSF, nginx, Cloudflare y Debian revisada el 10/11/2026. Son resultados de un banco de pruebas, no datos de los servidores de RichHost.
Qué bloquean realmente Fail2Ban y CSF
Fail2Ban se compone de filtros (expresiones regulares para las líneas del log), jails (qué log leer y cuántas coincidencias contar) y acciones (qué hacer con la IP). Por defecto, la acción mete la dirección en un conjunto de nftables o iptables, y el kernel descarta sus paquetes hacia los puertos indicados. Por tanto, un bloqueo se aplica a la IP de origen de un paquete, no a un usuario, a un sitio ni a un ataque como tal.
| Herramienta | Lee | Bloquea | No bloquea |
|---|---|---|---|
| Fail2Ban | Archivos de log o journald | La IP en los puertos de la jail (o en todos los puertos con recidive) | Una botnet en la que cada dirección se queda por debajo de maxretry; peticiones a través de un proxy cuando el bloqueo está en el firewall del servidor |
| CSF + LFD | Logs en las rutas de csf.conf, cada 5 segundos (LF_PARSE) | La IP, de forma permanente o temporal; CSF define por sí mismo los puertos abiertos | Lo mismo, y además IPv6 está desactivado por defecto (IPV6 = “0”) |
| ModSecurity | La propia petición HTTP | Peticiones maliciosas concretas (WAF) | Adivinación lenta de contraseñas con peticiones bien formadas |
Algo que conviene saber sobre CSF: la empresa ConfigServer (Way to the Web Ltd) cerró el 31 de agosto de 2025. El código está bajo GPLv3 y sigue vivo en forks: cPanel mantiene una versión solo para servidores con cPanel y WHM, y existen forks de la comunidad para Debian y RHEL. Para un VPS nuevo sin panel de control, Fail2Ban desde el repositorio de la distribución es la opción más sencilla.
Un jail.local funcional para SSH, nginx y WordPress
Nunca edites jail.conf: una actualización del paquete lo sobrescribe. Pon tu configuración en /etc/fail2ban/jail.local. El banco funcionó exactamente con estas secciones, solo que con tiempos de bloqueo cortos para ver varios ciclos en pocos minutos.
# /etc/fail2ban/jail.local (Debian 12 / Ubuntu 24.04, Fail2Ban 1.0.x) [DEFAULT] ignoreip = 127.0.0.1/8 ::1 203.0.113.10 bantime = 1h findtime = 10m maxretry = 5 bantime.increment = true bantime.maxtime = 1w backend = auto banaction = nftables banaction_allports = nftables[type=allports] [sshd] enabled = true port = ssh # no /var/log/auth.log: read the journal backend = systemd [nginx-http-auth] enabled = true port = http,https logpath = /var/log/nginx/error.log [wordpress] enabled = true filter = wordpress-hard port = http,https logpath = /var/log/nginx/access.log # behind Cloudflare or another proxy: # action = %(action_)s # nginx-deny [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d maxretry = 3
Lo importante:
- maxretry y findtime fijan el umbral: 5 coincidencias en 10 minutos. Una coincidencia no es una contraseña. Una conexión SSH con un usuario inexistente genera dos líneas (
Invalid useryFailed password), por eso en el banco el bloqueo llegó tras la tercera conexión. - bantime.increment alarga el bloqueo de las direcciones que ya fueron bloqueadas antes, y recidive lee el propio log de Fail2Ban y cierra todos los puertos a quien haya sido bloqueado tres veces en un día.
- backend:
autopara archivos,systemdpara el journal. El paquete de Ubuntu 24.04 incluyejail.d/defaults-debian.confconbackend = systemdpara todas las jails, así que una jail de nginx sin unbackend = autoexplícito lee el journal de systemd y nunca ve ni una línea de access.log. - ignoreip: tu dirección fija o la de tu VPN.
ignoreselfestá activado por defecto y solo cubre las direcciones del propio servidor.
El paquete no trae filtro para WordPress, así que escribimos uno para el log combined de nginx. WordPress responde a un inicio de sesión fallido en wp-login.php con 200 y a uno correcto con 302, por lo que solo cuenta el 200. También cuenta cualquier POST a xmlrpc.php.
# /etc/fail2ban/filter.d/wordpress-hard.conf (nginx combined log)
[Definition]
failregex = ^<HOST> \S+ \S+ \[\] "POST /wp-login\.php(?:\?\S*)? HTTP/[\d.]+" 200
^<HOST> \S+ \S+ \[\] "POST /xmlrpc\.php(?:\?\S*)? HTTP/[\d.]+"
ignoreregex =
Prueba el filtro con un log real antes de activarlo. En el banco, fail2ban-regex encontró 2 coincidencias en 8 líneas: el inicio de sesión fallido y la petición XML-RPC, y dejó fuera el inicio de sesión correcto del administrador. Comandos para el día a día:
fail2ban-client status sshd fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-hard.conf fail2ban-client get wordpress logpath fail2ban-client set sshd unbanip 203.0.113.10 fail2ban-client get recidive banip --with-time
Qué mostró el banco de pruebas
El atacante probó contraseñas SSH tan rápido como permite sshpass, una contraseña por conexión. Una ejecución, abreviada:
$ att_ssh.sh cw33att 9
17:01:47.105 try 1: admin@10.33.0.1: Permission denied (publickey,password).
17:01:47.648 try 4: admin@10.33.0.1: Permission denied (publickey,password).
17:01:47.853 try 5: ssh: connect to host 10.33.0.1 port 22: Connection refused
$ grep sshd /srv/cw33/log/fail2ban.log
17:01:47,623 [sshd] Found 10.33.0.2 - 2026-11-10 17:01:47
17:01:47,804 [sshd] Ban 10.33.0.2
$ nft list table inet f2b-table
set addr-set-sshd { type ipv4_addr; elements = { 10.33.0.2 } }
tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
Repetimos la ráfaga 10 veces para cada método de lectura del log. Con pyinotify el bloqueo llegó entre 0,34 y 1,2 segundos después del primer intento, mediana de 0,5 segundos; con polling, entre 0,43 y 1,2 segundos, mediana de 0,56. Antes del bloqueo el bot consiguió pasar de 2 a 6 conexiones. Así que maxretry no es un límite exacto: un bot rápido supera el umbral mientras Fail2Ban todavía está leyendo el log.
Con los formularios web la diferencia es mayor. 8 peticiones POST a wp-login.php tardaron 0,21 segundos, y el bloqueo llegó 76 milisegundos después de la quinta coincidencia, cuando las peticiones 6, 7 y 8 ya tenían respuesta. En la jail nginx-http-auth, 7 contraseñas Basic Auth erróneas tardaron 64 milisegundos y el bloqueo llegó a los 0,35 segundos. Después, esa IP recibía connection refused en el puerto 80, mientras que la dirección del administrador seguía funcionando.
También probamos XML-RPC. Una sola petición system.multicall con 20 intentos de inicio de sesión es una única línea en el log de nginx, así que Fail2Ban la cuenta una vez. Sin embargo, WordPress 6.4.3 rechazó todos los intentos tras el primer fallo dentro de esa petición, incluso el último con la contraseña correcta. Si no necesitas XML-RPC (lo usan algunas apps móviles y Jetpack), bloquéalo en el servidor web, por ejemplo con una regla en el archivo .htaccess de tu hosting.
findtime, bantime y recidive
Lanzamos intentos lentos con findtime = 60s. Una conexión cada 35 segundos acabó bloqueada igualmente en la tercera conexión, aunque entre el primer y el tercer intento pasaron 71 segundos. La razón está en el código de Fail2Ban 1.0.2: cuando el intervalo supera findtime, el contador no se reinicia, sino que se recalcula a partir del ritmo de intentos. Una conexión cada 50 segundos nunca se bloqueó: 10 contraseñas en 200 segundos. Una jail paralela con findtime = 10m sobre las mismas líneas la habría bloqueado en la tercera conexión.
Probamos la escalada con un bloqueo intencionadamente corto de 20 segundos para ver varios ciclos:
# sshd bantime 20s + recidive (maxretry 3, findtime 1d, bantime 1w) 17:02:56 [sshd] Ban 10.33.0.2 17:03:15 [sshd] Unban 17:03:17 [sshd] Ban 10.33.0.2 17:03:36 [sshd] Unban 17:03:39 [sshd] Ban 10.33.0.2 17:03:39 [recidive] Ban 10.33.0.2 $ fail2ban-client get recidive banip --with-time 10.33.0.2 2026-11-10 17:03:39 + 604800 = 2026-11-17 17:03:39 # sshd bantime 20s + bantime.increment = true [sshd] Increase Ban 10.33.0.2 (2 # 40s -> 2026-11-10 17:07:05) [sshd] Increase Ban 10.33.0.2 (3 # 1m 20s -> 2026-11-10 17:08:26) [sshd] Increase Ban 10.33.0.2 (4 # 2m 40s -> 2026-11-10 17:11:06)
Recidive cerró todos los puertos TCP al atacante 47 segundos después del primer intento, y a partir de ahí también dejó de responder el sitio en el puerto 80. Con bantime.increment, una dirección conocida volvía a bloquearse tras una sola conexión, y el bloqueo se duplicaba cada vez: 20 segundos, 40, 1 minuto 20, 2 minutos 40.

Trampas que reprodujimos
Te bloqueas a ti mismo
El administrador del banco se equivocó de contraseña siete veces y quedó bloqueado en 1,1 segundos. SSH desde esa dirección dejó de responder, mientras el sitio seguía funcionando, porque la jail sshd solo cierra el puerto 22. Puedes desbloquearte desde la consola del proveedor o desde otra dirección con fail2ban-client set sshd unbanip. Cuando la dirección pasó a ignoreip, seis inicios de sesión fallidos seguidos solo produjeron Ignore 10.33.0.3 by ip en el log. No bajes maxretry a 1 o 2: la primera clave equivocada o una contraseña antigua guardada en un cliente te dejará fuera.
El backend systemd y el auth.log que no existe
Debian 12 ya no instala rsyslog por defecto, así que /var/log/auth.log puede no existir y los logs de SSH solo están en journald. Por eso el paquete de Ubuntu define backend = systemd. Reprodujimos en el banco las dos combinaciones incorrectas:
# Ubuntu 24.04 package: /etc/fail2ban/jail.d/defaults-debian.conf [DEFAULT] banaction = nftables banaction_allports = nftables[type=allports] backend = systemd # sshd logs to a file, the jail reads the journal: 8 failed logins [sshd] Added journal match for: '_SYSTEMD_UNIT=sshd.service + _COMM=sshd' |- Currently failed: 0 |- Total failed: 0 # backend = auto, /var/log/auth.log does not exist ERROR Failed during configuration: Have not found any log file for sshd jail
En el primer caso Fail2Ban funciona y muestra ceros durante un ataque. En el segundo, el servidor ni siquiera arranca. Otro detalle: desde OpenSSH 9.8 algunos mensajes proceden de un proceso llamado sshd-session, y el filtro sshd lo tiene en cuenta solo a partir de Fail2Ban 1.1.1 (agosto de 2026, según el changelog del proyecto). Así que después de cada actualización haz un inicio de sesión fallido y comprueba que Total failed ha subido. No probamos con el propio journal de systemd: el contenedor no lo tiene.
IPv6
En un kernel con IPv6, Fail2Ban 1.0.2 bloqueó 2001:db8:33::2 igual que una dirección IPv4: la acción de nftables mantiene un conjunto aparte para IPv6. Pero un host suele recibir un /64 completo, y cambiar de dirección dentro de él no cuesta nada:
$ fail2ban-client status sshd
`- Banned IP list: 2001:db8:33::2
$ nft list table inet f2b-table
set addr6-set-sshd { type ipv6_addr; elements = { 2001:db8:33::2 } }
tcp dport 22 ip6 saddr @addr6-set-sshd reject with icmpv6 port-unreachable
# one host, 10 addresses of the same /64, 2 tries each
20 x Permission denied, Currently banned: 0
20 conexiones desde 10 direcciones de un mismo /64 no produjeron ningún bloqueo. Si tu sitio usa IPv6 en el servidor, el acceso SSH solo con clave es la defensa fiable, y los formularios web necesitan límites en la propia aplicación. En CSF hay que activar IPv6 por separado: el csf.conf original trae IPV6 = "0".
Un sitio detrás de Cloudflare u otro proxy
Detrás de un proxy, todas las peticiones llegan al servidor desde las direcciones del proxy. Colocamos un nginx delante del sitio en lugar de una CDN y pasamos por tres etapas:
# 1. no real_ip: nginx sees only the proxy 10.33.0.4 - - [10/Nov/2026:17:22:31 +0000] "POST /wp-login.php HTTP/1.0" 200 NOTICE [wordpress] Ban 10.33.0.4 admin via proxy: 502 (connect() failed (111: Connection refused) while connecting to upstream) # 2. real_ip on, ban in nftables NOTICE [wordpress] Ban 10.33.0.2 attacker via proxy: 200 200 200 # 3. real_ip + nginx-deny deny 10.33.0.2; attacker via proxy: 403 403 403 admin via proxy: 200
Sin real_ip, Fail2Ban bloqueó al propio proxy y el administrador recibió un 502. Con real_ip el log mostraba la dirección real, pero el bloqueo en nftables no servía de nada: los paquetes del atacante siguen llegando desde el proxy. Solo funcionó un bloqueo dentro de nginx, donde tras real_ip $remote_addr es el cliente real. Para Cloudflare existe además la acción cloudflare-token, que añade una regla a través de la API de Cloudflare. Una acción lista para nginx:
# /etc/fail2ban/action.d/nginx-deny.conf [Definition] actionstart = touch <denyfile> actionstop = truncate -s 0 <denyfile> && <nginx_reload> actionban = echo "deny <ip>;" >> <denyfile> && <nginx_reload> actionunban = sed -i '/^deny <ip>;$/d' <denyfile> && <nginx_reload> [Init] denyfile = /etc/nginx/fail2ban-deny.conf nginx_reload = nginx -s reload # /etc/nginx/conf.d/realip.conf (http level) # every range from https://www.cloudflare.com/ips-v4 and /ips-v6 set_real_ip_from 173.245.48.0/20; set_real_ip_from 103.21.244.0/22; # ... real_ip_header CF-Connecting-IP; include /etc/nginx/fail2ban-deny.conf;
Confía en la cabecera solo desde los rangos de direcciones del proxy. Con set_real_ip_from 0.0.0.0/0 en el banco, el atacante se conectó directamente y envió una dirección nueva en X-Forwarded-For cada vez: 12 intentos quedaron registrados como 12 IP distintas, sin bloqueo. La lista de rangos de Cloudflare cambia (15 redes IPv4 el 10/11/2026), así que actualízala de forma programada.
Cambió la ruta del log
Fail2Ban sobrevivió a una rotación normal (renombrado más nginx -s reopen) y bloqueó el siguiente ataque. Pero cuando movimos el access_log del sitio a un archivo aparte, como hacen los paneles de control, la jail siguió leyendo el archivo antiguo: 10 inicios de sesión fallidos, cero coincidencias nuevas. Tras un logpath con el patrón /var/log/nginx/domains/*.log y fail2ban-client reload wordpress, los bloqueos volvieron. Comprueba qué archivos lee realmente una jail con fail2ban-client get wordpress logpath. Explicamos dónde están los logs y cómo leerlos en nuestra guía sobre los logs de acceso y de errores.
CSF y LFD: qué saber si ya está instalado
Con el csf.conf original, LFD bloquea SSH tras 5 fallos (LF_SSHD = "5") y lo hace de forma permanente (LF_SSHD_PERM = "1"), cuenta los fallos durante una hora (LF_INTERVAL = "3600") y convierte una dirección en bloqueo permanente tras 4 bloqueos temporales en un día (LF_PERMBLOCK). Una instalación nueva tiene TESTING = "1": cron borra las reglas cada 5 minutos y LFD no arranca, así que no hay protección real hasta que lo pongas a 0. El readme de CSF dice claramente que detrás de CloudFlare iptables no puede bloquear la dirección real, y que su integración con la API solo funciona para los disparadores de ModSecurity y cxs. Si CSF y LFD ya están funcionando, no añadas encima una jail SSH de Fail2Ban: las dos herramientas mantendrían listas de bloqueo separadas en un mismo firewall.
Qué más detiene los ataques de contraseñas a SSH
Fail2Ban reduce el ruido del log, pero la defensa principal de SSH está en otra parte. Cambia al acceso SSH solo con clave con PasswordAuthentication no, y adivinar contraseñas deja de tener sentido. Desactiva el acceso como root (PermitRootLogin no). Para servidores críticos, OpenSSH admite llaves de hardware FIDO2 desde la versión 8.2, y un módulo PAM añade códigos de un solo uso TOTP. Si gestionas muchos servidores, echa un vistazo a CrowdSec: comparte entre sus participantes listas de direcciones atacantes. Y una vez por trimestre haz una auditoría de seguridad del sitio: encuentra puertos abiertos y archivos olvidados que Fail2Ban nunca ve.
Si X, haz Y
| Si | Haz |
|---|---|
| Tu sitio está en un hosting compartido | No instalas ni Fail2Ban ni CSF; el firewall del servidor lo gestiona el proveedor. Para WordPress: contraseña fuerte, inicio de sesión en dos pasos, XML-RPC cerrado si no se usa |
| Tu propio VPS con Debian o Ubuntu, sin panel | Fail2Ban desde el repositorio: sshd, recidive, bantime.increment; comprueba el backend con un inicio de sesión fallido |
| SSH ya solo acepta claves | Mantén la jail sshd para tener un log limpio, pero no bajes maxretry: la clave es la verdadera protección |
| El sitio está detrás de Cloudflare u otro proxy | real_ip solo para los rangos del proxy, bloqueo en nginx (nginx-deny) o mediante la API de Cloudflare; un bloqueo en nftables no sirve |
| Tienes una dirección IP dinámica | Pon la dirección de tu VPN o de tu oficina en ignoreip y ten a mano la consola del proveedor |
| El servidor es accesible por IPv6 | Comprueba que los bloqueos entran en el conjunto IPv6; recuerda la evasión con /64; activa IPV6 en CSF |
| Los intentos son lentos, uno cada pocos minutos | Aumenta findtime y apóyate en recidive con findtime 1d |
| Tu panel escribe un log aparte por dominio | Un patrón en logpath y una comprobación con fail2ban-client get <jail> logpath |
| El servidor ya funciona con cPanel y CSF | Mantén LFD, desactiva TESTING, no dupliques SSH en Fail2Ban |
Y en un hosting compartido
En un hosting compartido, el proveedor gestiona el servidor, el firewall y la protección contra fuerza bruta a nivel de servidor, y los clientes no instalan Fail2Ban ni CSF. RichHost ofrece hosting compartido y dominios, sin planes VPS, así que esta guía está pensada para quien tiene su propio servidor y para entender qué ocurre en la máquina donde funciona tu sitio. En los planes de hosting compartido de RichHost (desde $8 al mes, panel de control propio con asistente de IA, Let’s Encrypt gratuito) tu parte del trabajo es el propio sitio: contraseñas, actualizaciones, XML-RPC cerrado y la lectura de los logs.
Fuentes (revisadas el 10/11/2026): Fail2Ban jail.conf; Fail2Ban ChangeLog; CSF (fork de cPanel): readme.txt y csf.conf; cPanel: fork propio de CSF y cierre de ConfigServer; nginx realip; Cloudflare: restaurar las IP reales de los visitantes; notas de la versión de Debian 12, sección 5.1.7.
Preguntas frecuentes
¿Necesito Fail2Ban si SSH solo acepta claves?
No es imprescindible, pero ayuda. La clave detiene la adivinación en sí, mientras que Fail2Ban elimina miles de líneas de ruido del log y, con recidive, cierra el resto de puertos a las direcciones insistentes.
¿Fail2Ban o CSF en 2026?
Para un VPS sin panel, Fail2Ban: está en los repositorios de Debian y Ubuntu y se desarrolla activamente (la versión 1.1.1 salió en agosto de 2026). CSF tiene sentido donde ya está integrado, sobre todo en servidores con cPanel.
¿Por qué Fail2Ban no bloquea nada si el log está lleno de fallos?
Lo más habitual es que la jail lea la fuente equivocada: journald en lugar de un archivo o al revés, o una ruta de log antigua. Revisa fail2ban-client status, get <jail> logpath y pasa el filtro por fail2ban-regex.
¿Puedo instalar Fail2Ban en un hosting compartido?
No, necesita root y acceso al firewall del servidor. En un hosting compartido protege el propio sitio: contraseña, inicio de sesión en dos pasos, XML-RPC limitado; la protección del servidor corresponde al proveedor.



