Ya lo he comentado muchas veces. En cuanto instalaba un nuevo servidor, siempre me decantaba por CSF + LFD como firewall y para bloquear ataques externos, pero en 2025, la empresa que gestionaba este script, decidió que ya era hora de dejarlo. Por eso ahora mismo estoy usando Firewalld y Fail2ban para sustituir a CSF. Te cuento cómo lo instalo en Rocky Linux 9 con Virtualmin (panel de control).
Requisitos previos
- Rocky Linux 9 en tu servidor (en mi caso, en Hetzner).
- Si quieres que te envíe mensajes de alerta, tienes que tener configurado sendmail.
- Si ya has instalado Virtualmin, automáticamente habrán instalado Fail2ban y Firewalld en tu servidor.
En caso contrario, tienes que instalar Firewalld en Rocky Linux con:
#Instalamos
dnf install firewalld -y
#Lo iniciamos (permite SSH de manera automatica)
systemctl start firewalld
#Comprobamos estado
systemctl status firewalld
Con este comando puedes comprobar qué servicios dejas pasar:
firewall-cmd --permanent --list-all
¿Quieres añadir nuevos servicios (si no lo haces, tu tráfico web no va a funcionar)?
firewall-cmd --permanent --add-service=http
#recargamos el firewall
firewall-cmd --reload

¿Qué servicios deberías añadir? Pues depende mucho de tu configuración, pero pueden ser estos:
- dns dns-over-tls http https imap imaps pop3 pop3s smtp smtp-submission smtps 10000-10100/tcp 80/udp 443/udp
Dato: 10000-10100/tcp son los puertos originales que necesita Virtualmin para funcionar.
¿Ha sido complicado pasar de CSF + LFD a Firewalld + Fail2ban? La verdad es que no, pero tienes que tener en cuenta que Fail2ban tiene cierta curva de aprendizaje, sobre todo cuando nos toca adaptar las fórmulas REGEX para que descubran correctamente los ataques en los logs del sistema.
Instalamos Fail2ban
Fail2ban no está disponible en los repositorios de Rocky Linux, por lo que tienes que instalar antes EPEL (es seguro):
sudo dnf install epel-release -y
Y luego ya puedes instalar Fail2ban:
#instalamos
sudo dnf install fail2ban -y
#Comprobamos que funciona
systemctl status fail2ban.service
Configuramos Fail2ban
Toda la configuración de Fail2ban está en el siguiente directorio: /etc/fail2ban. Allí vamos a tener que copiar jail.conf en un archivo local (para evitar que se sobreescriba):
sudo cp jail.conf jail.local

Y ahora lo editamos con el comando: sudo vi jail.local. Vas a tener muchos parámetros. Deberías hacer un backup de jail.local antes de hacer modificaciones importantes. Esto es lo que hacen:
- [DEFAULT]: Sección principal para parámetros globales que aplican a todas las cárceles (jails).
- ignoreip: Direcciones IP, hosts o redes que nunca serán baneadas.
- bantime: Duración del bloqueo (en segundos).
-1es baneo permanente. - findtime: Ventana de tiempo (en segundos) durante la cual se cuentan los fallos.
- maxretry: Número máximo de intentos fallidos antes del baneo.
- backend: Mecanismo de detección (ej: auto, pyinotify, systemd, polling).
- banaction: Acción a ejecutar para banear (por ejemplo, firewallcmd).
- destemail: Dirección de correo para notificaciones.
- sender: Dirección de correo del remitente de las alertas.
- mta: Agente de transferencia de correo para enviar emails.
- protocol: Protocolo a monitorear (tcp, udp, all). Por defecto es tcp.
- chain: Cadena de iptables donde se añaden las reglas.
port: Puertos a bloquear. Puede ser un número, nombre de servicio (ej: ssh) o all.- logpath: Ruta del archivo de log que monitorea el jail.
- enabled: Habilita (true) o deshabilita (false) un jail específico.
- Secciones de jails específicas: Por ejemplo, [sshd] o [nginx-http-auth].
- Aquí se redefinen cualquiera de los parámetros anteriores para personalizar el comportamiento de un servicio en particular. Solo es necesario configurar los que difieren de [DEFAULT].
- filter: Nombre del archivo de filtro (en /etc/fail2ban/filter.d/) que contiene las expresiones regulares para detectar fallos.
mode: Define el modo del jail- enabled: debe establecerse en true para activar el jail específico.
Puedes empezar en el Default con estos valores e ir probando:
[DEFAULT]
bantime = 1200
findtime = 900
maxretry = 5
mta = sendmail
destemail = tu_dirección_email
sender = root
action = %(action_mwl)s
banaction = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules
bantime.increment = true
bantime.maxtime = 604800
bantime.rndtime = 1800
bantime.overalljails = true
bantime.multipliers = 1 2 4 8 16 32
- Dato: Puedes configurar el bantime de forma incremental con bantime.increment = true
- action = %(action_mwl)s: Le estoy diciendo que Banee la IP y que me envíe un email detallado con contexto completo del ataque.
- Con banaction = firewallcmd-rich-rules y banaction_allports = firewallcmd-rich-rules le digo diciendo a Fail2Ban que use Firewalld (no iptables directamente) y que aplique el baneo mediante rich rules.
Y luego ir configurando los servicios que estés usando en tu servidor en un apartado específico, por ejemplo, para el jail apache-auth:
[apache-auth]
enabled = true
logpath = %(apache_error_log)s
/var/log/virtualmin/*_error_log
filter = apache-auth
backend = polling
maxretry = 3
findtime = 900
bantime = 21600
- Dato: Si te fijas, hay que comprobar muy bien dónde va a mirar Fail2ban los logs del sistema. Si no indicas la ruta del log correcta en el jail, Fail2ban no va a hacer nada. En Virtualmin, hay un log concreto para cada dominio del tipo (si tienes varios dominios, con poner un * los cubre todos):
- /home/tu_usuario_dominio/logs/php_log
- /var/log/virtualmin/tu_dominio_access_log
- /var/log/virtualmin/tu_dominio_error_log
También tienes que tener en cuenta una cosa importante. Tienes que comprobar que el filtro actúa bien con tus logs. Es posible que el filtro Regex de algunos no coincida con nada de lo que tengas en tus logs. Tienes que ir adaptándolo (puedes pedir ayuda a alguna IA como ChatGPT o Deepseek si el Regex se te da mal).
Resumiendo. El archivo jail le indica a Fail2ban qué es lo que tiene que hacer cuando el patrón que hay en el filtro coincide con algo en tus logs. En mi caso le estoy diciendo que bloquee la IP con Firewalld y me envíe un e-mail. ¿Qué pinta tienen esos filtros? Los tienes en el directorio /etc/fail2ban/filter.d y están llenos de expresiones regulares (REGEX). Tienes todos los filtros oficiales en el GitHub de Fail2ban. Por ejemplo, el de apache-auth.conf:
# Fail2Ban apache-auth filter
#
[INCLUDES]
# Read common prefixes. If any customizations available -- read them from
# apache-common.local
before = apache-common.conf
[Definition]
# Mode for filter: normal (default) and aggressive (allows DDoS & brute force detection of mod_evasive)
mode = normal
# ignore messages of mod_evasive module:
apache-pref-ign-normal = (?!evasive)
# allow "denied by server configuration" from all modules:
apache-pref-ign-aggressive =
# mode related ignore prefix for common _apache_error_client substitution:
apache-pref-ignore = <apache-pref-ign-<mode>>
prefregex = ^%(_apache_error_client)s (?:AH\d+: )?<F-CONTENT>.+</F-CONTENT>$
# auth_type = ((?:Digest|Basic): )?
auth_type = ([A-Z]\w+: )?
failregex = ^client (?:denied by server configuration|used wrong authentication scheme)\b
^user (?!`)<F-USER>(?:\S*|.*?)</F-USER> (?:auth(?:oriz|entic)ation failure|not found|denied by provider)\b
^Authorization of user <F-USER>(?:\S*|.*?)</F-USER> to access .*? failed\b
^%(auth_type)suser <F-USER>(?:\S*|.*?)</F-USER>: password mismatch\b
^%(auth_type)suser `<F-USER>(?:[^']*|.*?)</F-USER>' in realm `.+' (auth(?:oriz|entic)ation failure|not found|denied by provider)\b
^%(auth_type)sinvalid nonce .* received - length is not\b
^%(auth_type)srealm mismatch - got `(?:[^']*|.*?)' but expected\b
^%(auth_type)sunknown algorithm `(?:[^']*|.*?)' received\b
^invalid qop `(?:[^']*|.*?)' received\b
^%(auth_type)sinvalid nonce .*? received - user attempted time travel\b
^(?:No h|H)ostname \S+ provided via SNI(?:, but no hostname provided| and hostname \S+ provided| for a name based virtual host)\b
ignoreregex =
Este filtro apache-auth.conf está conectado al jail [apache-auth] que he puesto antes en jail.local.
Creamos filtros y jails nuevos
Los filtros que vienen por defecto no están mal para empezar, pero si tienes una instalación de WordPress, se pueden quedar cortos. Vale. Los pasos a seguir para crear un filtro y un nuevo jail para WordPress son los siguientes.
Creamos un nuevo filtro para WordPress
Lo hacemos en forma de nuevo archivo .conf en /etc/fail2ban/filter.d, por ejemplo, wp-protect.conf y ponemos lo siguiente dentro:
[Definition]
failregex = <HOST>.*"(POST|GET).*?(wp-login\.php|xmlrpc\.php|account/signin).*" 200
Donde protegemos los siguientes puntos de WordPress:
- wp-login.php: Página de login estándar de WordPress.
- xmlrpc.php: Interfaz remota de WordPress, usada por apps o ataques de fuerza bruta.
- account/signin: otra ruta de login
Ahora tenemos que crear otro archivo de jail en /etc/fail2ban/jail.d llamado wp-protect.conf, y ponemos en su interior:
[wp-protect]
enabled = true
backend = auto
port = http,https
filter = wp-protect
logpath = /var/log/virtualmin/*_access_log
maxretry = 4
findtime = 900
bantime = 21600
ignoreip =
Es importante poner el backend en auto o polling para que Fail2ban mire en los archivos de logs, el nombre del filtro correcto, el logpath (en este caso mis logs de Virtualmin para ese dominio) y luego ir modificando el resto de parámetros para que se ajusten a tu sistema. Puedes empezar con los que te pongo.
Ahora, antes de recargar Fail2ban, podemos probar que el filtro funciona con nuestros logs:
sudo fail2ban-regex /ruta/al/log/de/wordpress /etc/fail2ban/filter.d/wp-protect.conf
- Te tiene que salir algo como esto: Lines: 184345 lines, 1731 ignored, 3058 matched, 179556 missed
O ver las estadísticas generales del jail con:
sudo fail2ban-client status wp-protect
Y saldra algo así:
Status for the jail: wp-protect
|- Filter
| |- Currently failed: 0
| |- Total failed: 1532
| - File list: /var/log/virtualmin/*_access_log
- Actions
|- Currently banned: 28
|- Total banned: 29
- Banned IP list:
Ahora solo tenemos que recargar la configuración de Fail2ban para que el jail entre en acción:
sudo systemctl reload fail2ban
Creamos un filtro nuevo para evitar bots
El filtro que viene para evitar bots (apache-botsearch.conf) que escaneen tus páginas en Fail2ban no funciona muy bien (bots antiguos y filtros desactualizados). No pasa nada. Puedes crear tú uno nuevo con los bots que han recopilado en el firewall 8G de PerisablePress. Te descargas el archivo txt y buscas las líneas de bots a bloquear en 8G:[USER AGENT] (yo he seleccionado unos cuantos y he quitado otros) y luego creas un filtro nuevo llamado badbots.conf:
[Definition]
badbotscustom = python-httpx|Python-urllib|Go-http-client|aiohttp|Python/3|python-requests|Barkrowler|LeakIX|l9scan|PaloAlto|CortexXpanse|acapbot|acoonbot|alexibot|asterias|attackbot|awario|backdor|becomebot|binlar|blackwidow|blekkobot|blex|blowfish|bullseye|bunnys|butterfly|careerbot|casper|censysinspect|checkpriv|cheesebot|cherrypick|chinaclaw|choppy|clshttp|cmsworld|copernic|copyrightcheck|cosmos|crawlergo|crescent|datacha|demon|diavol|discobot|dittospyder|dotbot|dotnetdotcom|dumbot|econtext|emailcollector|emailsiphon|emailwolf|eolasbot|eventures|extract|eyenetie|feedfinder|flaming|flashget|flicky|foobot
badbots = fuck|g00g1e|getright|gigabot|go-ahead-got|gozilla|grabnet|grafula|harvest|heritrix|httracks?|icarus6j|imagesiftbot|jetbot|jetcar|jikespider|kmccrew|leechftp|libweb|liebaofast|linkscan|linkwalker|lwp-download|majestic|masscan|mauibot|miner|mechanize|mj12bot|morfeus|moveoverbot|mozlila|nbot|netmechanic|netspider|nicerspro|nikto|ninja|nominet|nutch|octopus|pagegrabber|petalbot|planetwork|postrank|proximic|purebot|queryn|queryseeker|radian6|radiation|realdownload|remoteview|rogerbot|scan|scooter|seekerspid|semalt|siclab|sindice|sistrix|sitebot|siteexplorer|sitesnagger|skygrid|smartdownload|snoopy|sosospider|spankbot|spbot|sqlmap|stackrambler|stripper|sucker|surftbot|sux0r|suzukacz|suzuran|takeout|teleport|telesoft|true_robots|turingos|turnit|vampire|vikspider|voideye|webleacher|webreaper|webstripper|webvac|webviewer|webwhacker|winhttp|wwwoffle|woxbot|xaldon|xxxyy|yamanalab|yioopbot|youda|zeus|zmeu|zune|zyborg
failregex = ^<HOST>\s+-\s+-\s+\[.*\]\s+"(GET|POST|HEAD)\s+.*\s+HTTP/.*"\s+\d+\s+[-\d]+\s+"[^"]*"\s+"[^"]*(?:%(badbots)s|%(badbotscustom)s)[^"]*"
# Ignorar regex
ignoreregex = (Googlebot|Bingbot|Amazonbot)
Aquí lo importante es que la línea failregex realmente filtre los datos correctos en tu log. Yo la he tenido que adaptar a los míos. Puedes poner en ignoreregex el nombre de los bots que no quieres bloquear (por si acaso).
Ahora tenemos que crear otro archivo de jail en /etc/fail2ban/jail.d llamado badbots.conf, y ponemos en su interior:
[badbots]
enabled = true
filter = badbots
port = http,https
logpath = /var/log/virtualmin/*_access_log
maxretry = 3
bantime = 86400
findtime = 1800
backend = auto
- En mi caso, los logs tienen una pinta como esta: dirección_IP – – [19/Dec/2025:10:28:55 +0100] «GET /trucos-y-estrategias-de-clash-royale-que-nunca-nadie-te-habia-contado/ HTTP/2.0» 200 48504 «-» «Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36». El regex lo pilla perfectamente. Revisa los tuyos.
Como antes, probamos el filtro y luego recargamos la configuración de Fail2ban. Es importante que revises que el filtro recoge bien los positivos en tu log. En caso contrario, Fail2ban no va a hacer nada.
- ¡Ojo! Recuerda que puedes bloquear también estos bots en Cloudflare y su Firewall. Puedes hacer las dos cosas para incrementar tu seguridad.
¡Ojo! Fundamental activar en cualquier caso el Jail Recidive, que se encarga de bloquear permanentemente a las peores IP.
- Para comprobar lo que hace Fail2ban en tu servidor, debería consultar su log en: /var/log/fail2ban.log, sobre todo cuando un jail no funciona o por si funciona demasiado bien y tienes falsos positivos.
Virtualmin, Fail2ban y Firewalld
Como último apunte, decirte que Virtualmin proporciona interfaz gráfica tanto para Firewalld como para Fail2ban. Está disponible en Webmin > Networking > FirewallD y Webmin > Networking > Fail2ban Intrusion Detector.

Puedes modificar toda la configuración en caso de que no te guste hacerlo todo mediante la Terminal. Yo la uso bastante para controlar las IP que se van baneando (Jail Status):

Conclusión
Te habrás dado cuenta de que Fail2ban es una herramienta poderosa para proteger tu servidor VPS de ataques externos, pero que depende mucho de la configuración que apliques para que funcione bien o no haga absolutamente nada.
Configurar bien las reglas REGEX es fundamental, pero también elegir el tiempo para encontrar los positivos (findtime) y cuántas veces permites que accedan a tu web (maxretry) antes de proceder al bloqueo de las solicitudes (bantime).
El mejor consejo que te puedo dar es que vayas revisando los logs de tu servidor y que vayas adaptando los patrones REGEX para bloquear a los bots y atacantes que más te molesten. Tampoco está de más que configures Cloudflare y firewall delante de tu web para evitar más ruido en los logs. O que actives el firewall de proveedor de hosting, por ejemplo, en Hetzner.
Si tienes tu web detrás del proxy de Cloudflare, recuerda que tienes que activar mod_remoteip en Apache para poder ver las IPs reales de tus visitantes y no bloquear las IPs de Cloudflare.
Referencias: