Configurar fail2ban y bloquear ataques de fuerza bruta de forma automática
Cómo configurar fail2ban de forma limpia mediante jail.local, poner en marcha el jail sshd en las cuatro distribuciones LTS actuales, comprobar los bloqueos y volver a levantarlos, y qué errores te frenan por el camino.
Todo servidor con una dirección IPv4 pública empieza a recibir intentos de inicio de sesión en el puerto 22 pocos minutos después del primer aprovisionamiento. No se trata de un ataque dirigido, sino de simple ruido de fondo. fail2ban lee las líneas de log de esos intentos fallidos y hace que el firewall bloquee la IP de origen durante un tiempo definido. La herramienta está empaquetada en todas las distribuciones que se tratan aquí y queda lista en pocos minutos. Las trampas están en otra parte: en Debian 12, fail2ban arranca a menudo con un error de configuración, en algunos sistemas parece bloquear sin que se descarte ni un solo paquete, y una línea imprudente en jail.local te deja fuera de tu propio servidor.
Este artículo cubre exactamente esos puntos, para Debian 13, Debian 12, Ubuntu 24.04 LTS y Ubuntu 22.04 LTS.
Por qué jail.local y nunca jail.conf
El archivo /etc/fail2ban/jail.conf pertenece al paquete. Cada actualización puede sobrescribirlo, y en el mejor de los casos dpkg te preguntará, mientras que en el peor, durante una actualización desatendida, no preguntará nada. Por eso tus ajustes viven en /etc/fail2ban/jail.local. Ese archivo no existe tras la instalación, lo creas tú mismo, y solo debe contener los valores que realmente cambias.
El orden en el que fail2ban lee es importante. La página del manual jail.conf(5) lo indica de forma explícita:
jail.confjail.d/*.confen orden alfabéticojail.localjail.d/*.localen orden alfabético
Los archivos leídos más tarde ganan. En concreto, esto significa que tu jail.local también sobrescribe los valores predeterminados de /etc/fail2ban/jail.d/defaults-debian.conf, el archivo que trae el paquete de la distribución. Muchas guías afirman lo contrario y por eso recomiendan un archivo propio dentro de jail.d/. No hace falta. jail.d/ solo tiene sentido si quieres desplegar jails por separado mediante gestión de configuración.
Lo que tu distribución ya trae definido
La mayor diferencia entre los cuatro sistemas no está en fail2ban en sí, sino en ese único archivo que el paquete coloca en /etc/fail2ban/jail.d/defaults-debian.conf. Míralo antes que nada:
cat /etc/fail2ban/jail.d/defaults-debian.conf
| Sistema | fail2ban | Contenido de defaults-debian.conf |
|---|---|---|
| Debian 13 | 1.1.0 | banaction = nftables, banaction_allports = nftables[type=allports], y para [sshd] además backend = systemd, journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd y enabled = true |
| Debian 12 | 1.0.2 | solo [sshd] y enabled = true |
| Ubuntu 24.04 | 1.0.2 | banaction = nftables, banaction_allports = nftables[type=allports], backend = systemd, además de [sshd] con enabled = true |
| Ubuntu 22.04 | 0.11.2 | solo [sshd] y enabled = true |
De ahí se deriva casi todo lo demás. Debian 13 y Ubuntu 24.04 leen de fábrica el journal de systemd y bloquean mediante nftables. Debian 12 y Ubuntu 22.04 recurren a los valores predeterminados de jail.conf, es decir, banaction = iptables-multiport y backend = auto. Y auto no significa en absoluto "busca tú la opción correcta". El comentario en jail.conf dice que auto prueba por orden pyinotify y polling. Ambos son métodos basados en archivos. El valor auto nunca elige el journal. Sin el archivo /var/log/auth.log, ahí el jail sshd se queda mirando al vacío.
Instalación y primera comprobación
apt update
apt install -y fail2ban
En Debian 12 y Ubuntu 22.04 hace falta un paquete más si quieres analizar el journal en lugar de un archivo de log:
apt install -y python3-systemd
En Debian 13, python3-systemd es una dependencia estricta del paquete y ya está presente. En Debian 12 solo está recomendado, y esa es la fuente de errores más habitual en ese sistema. Después comprueba la versión y el estado:
apt-cache policy fail2ban
fail2ban-client --version
systemctl status fail2ban --no-pager
Si aquí ya aparece active (running), tienes medio camino hecho. Si aparece failed, salta directamente a la sección sobre los mensajes de error.
Construir tu propia jail.local
Crea /etc/fail2ban/jail.local. Esta versión funciona en los cuatro sistemas, siempre que python3-systemd esté instalado:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.7
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 7d
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
enabled = true
backend = systemd
port = ssh
maxretry = 4
bantime = 2h
El significado de los valores, en claro: findtime es la ventana de tiempo deslizante, maxretry el número de intentos fallidos dentro de ella y bantime la duración del bloqueo. Cuatro intentos fallidos en diez minutos llevan aquí a dos horas de bloqueo. Las indicaciones de tiempo admiten sufijos como m, h, d y w; un número a secas significa segundos. Un bantime de -1 bloquea de forma permanente.
La parte interesante es bantime.increment. Con eso, fail2ban duplica el tiempo de bloqueo cada vez que la misma IP reincide, con el tope de bantime.maxtime. Una errata puntual de un usuario real cuesta dos horas, mientras que un bot insistente acaba en una semana tras unas pocas rondas. Esta función existe desde fail2ban 0.11, así que también está disponible en Ubuntu 22.04. Para que siga vigente después de un reinicio, la base de datos tiene que conservar el historial, algo que se controla con dbpurgeage en /etc/fail2ban/fail2ban.conf, un día por defecto. Si pones bantime.maxtime = 7d, sube también dbpurgeage en un fail2ban.local.
Dos trampas en este bloque. Primera, port = ssh: se resuelve a través de /etc/services al puerto 22. Si tu servicio SSH escucha en 2222, ahí tiene que poner port = 2222, porque de lo contrario se crea una regla de firewall para un puerto en el que nadie llama. Segunda, ignoreip: añade ahí la IP fija de tu oficina, pero nunca una red entera de un proveedor. Y no te fíes solo de eso. El valor ignoreself ya está en true por defecto y protege las IP del propio servidor.
Diferencias según el sistema
En Debian 12 tienes dos caminos equivalentes. O te quedas con backend = systemd e instalas python3-systemd, o instalas rsyslog, omites backend y trabajas con /var/log/auth.log. Además, en Debian 12 conviene fijar banaction de forma consciente. El paquete recomienda nftables o iptables, y apt suele instalar solo nftables. El valor predeterminado de jail.conf, en cambio, es iptables-multiport. En una instalación ligera, fail2ban acaba llamando a un binario que ni siquiera está.
En Ubuntu 22.04, rsyslog viene en la instalación estándar de servidor, /var/log/auth.log existe y el jail funciona sin tocar nada. Si ahí no quieres cambiar nada, omite simplemente backend y banaction en tu jail.local. fail2ban 0.11.2 conoce la acción nftables, pero en este sistema el camino por iptables-multiport está más trillado.
En Debian 13 y Ubuntu 24.04, tu jail.local coincide con lo que ya define el paquete. Eso no hace daño, solo vuelve la configuración explícita y, por tanto, legible.
Aplicar y comprobar que de verdad surte efecto
Releer la configuración sin perder los bloqueos existentes:
fail2ban-client reload
En Debian 12 y Ubuntu 24.04, fail2ban 1.0.2 muestra al arrancar y al recargar el aviso 'allowipv6' not defined in 'Definition'. Es inofensivo, el servicio sigue trabajando con normalidad.
Un systemctl restart fail2ban rara vez es necesario y descarta los bloqueos en curso. Después, las consultas de estado:
fail2ban-client ping
fail2ban-client status
fail2ban-client status sshd
La última salida es la significativa. Muestra Currently failed, Total failed, el número de bloqueos y la lista de direcciones bloqueadas. Justo aquí terminan la mayoría de las guías, y justo aquí empieza el problema: un jail que arranca limpiamente y anuncia "0 banned" puede estar completamente ciego. Un jail sin aciertos se ve exactamente igual que un jail que lee la fuente de logs equivocada.
Tres comprobaciones que hacen visible esa diferencia. Primera: ¿ve el filtro esas líneas siquiera?
fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf
Con un backend basado en archivos, en su lugar:
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
Al final aparece una línea como Lines: 4211 lines, 0 ignored, 137 matched, 4074 missed. Si ahí pone 0 matched aunque en el log se vean intentos fallidos con claridad, el filtro o la selección del journal no encajan.
Segunda: ¿qué filtro de journal usa realmente el jail?
fail2ban-client get sshd journalmatch
La respuesta varía según el sistema, y esa diferencia importa. Debian 13 devuelve _SYSTEMD_UNIT=ssh.service + _COMM=sshd, mientras que Ubuntu 24.04 devuelve _SYSTEMD_UNIT=sshd.service + _COMM=sshd. Es decir, ahí la unit se llama sshd.service y en Debian ssh.service. En Debian 12 y Ubuntu 22.04, tal como vienen de fábrica, la respuesta es No journal match filter set, porque ahí el backend systemd no está preconfigurado y el jail lee un archivo de log. Eso no es un error, sino la confirmación de que no existe ningún filtro de journal. Solo con backend = systemd en tu jail.local pasa a haber uno efectivo en esos sistemas.
Las distintas condiciones se separan con + y se combinan como un O lógico. Por tanto, _SYSTEMD_UNIT=ssh.service + _COMM=sshd encaja con todo lo que venga de la unit ssh.service o con cualquier proceso llamado sshd. Para la contraprueba en el journal usa exactamente el nombre de unit que aparece en la salida anterior:
journalctl -u ssh --no-pager -n 50
journalctl _COMM=sshd --no-pager -n 50
En Ubuntu 24.04, la primera línea es en consecuencia journalctl -u sshd --no-pager -n 50. Quien use aquí el nombre equivocado verá una salida vacía y después buscará por el lado que no es.
Si aquí aparecen líneas con Failed password for invalid user pero fail2ban no cuenta nada, la causa está en la selección del journal. Esto se vuelve relevante si ejecutas OpenSSH desde backports o cambias a activación por socket. Desde OpenSSH 9.8, el proceso hijo se llama sshd-session y ya no sshd, y con activación por socket la unit deja de llamarse ssh.service. En ese caso añade en tu jail.local (en Ubuntu 24.04 con _SYSTEMD_UNIT=sshd.service):
[sshd]
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
Tercera, la prueba dura. Bloquea a mano una dirección de la red de documentación y mira si llega al firewall:
fail2ban-client set sshd banip 203.0.113.10
El comando responde con 1, es decir, una dirección bloqueada. Con qué herramienta compruebas ese bloqueo depende de la banaction efectiva, y esa viene preconfigurada de forma distinta según la distribución:
| Sistema | banaction de fábrica | Comprobación del bloqueo activo |
|---|---|---|
| Debian 13 | nftables | nft list table inet f2b-table |
| Ubuntu 24.04 | nftables | nft list table inet f2b-table |
| Debian 12 | iptables-multiport | iptables -n -L f2b-sshd |
| Ubuntu 22.04 | iptables-multiport | iptables -n -L f2b-sshd |
El Debian 11 más antiguo se comporta como Debian 12. De dónde viene la diferencia lo ves en /etc/fail2ban/jail.d/defaults-debian.conf: solo Debian 13 y Ubuntu 24.04 traen ahí un banaction = nftables, y los demás se quedan con el valor predeterminado iptables-multiport de jail.conf. Quien no quiera ir distinguiendo esto servidor por servidor pone banaction = nftables en su propia jail.local, tal como en el ejemplo de más arriba, y después comprueba en todas partes con nft.
Con la acción nftables existe una tabla f2b-table en la familia inet, dentro de ella una chain f2b-chain con prioridad filter - 1 y un set llamado addr-set-sshd. Ahí tiene que figurar 203.0.113.10 como elemento. En estos sistemas no existe ninguna cadena f2b-sshd, y iptables -n -L f2b-sshd responde ahí con No chain/target/match by that name. A la inversa, nft list table inet f2b-table devuelve en Debian 12 y Ubuntu 22.04 Error: No such file or directory mientras ahí trabaje iptables-multiport. Ninguno de los dos mensajes demuestra que la configuración esté rota, solo que el comando no corresponde a la acción configurada.
Con la acción iptables, 203.0.113.10 tiene que aparecer en consecuencia en la cadena f2b-sshd. Solo cuando la IP asoma en el firewall queda cerrada la cadena que va del log hasta el paquete descartado. No olvides limpiar después.
Levantar los bloqueos
Liberar una única dirección de un jail concreto:
fail2ban-client set sshd unbanip 203.0.113.10
Una dirección en todos los jails de golpe:
fail2ban-client unban 203.0.113.10
Y, en caso de emergencia, todo a cero:
fail2ban-client unban --all
El último comando es el que necesitas si te has bloqueado a ti mismo y estás sentado ante una consola de emergencia del servidor. Levanta todos los bloqueos sin reiniciar el servicio. Si ya no consigues llegar al servidor de ninguna forma, solo queda el camino de la consola en el área de cliente. En los servidores root KVM y los servidores dedicados de KernelHost accedes ahí a una sesión VNC que funciona con independencia de SSH. Ese es también el motivo por el que no deberías poner bantime = -1 en un servidor sin una segunda vía de acceso.
Un bloqueo que levantas vuelve de inmediato si los intentos fallidos todavía se están contando dentro de la ventana de tiempo. Por eso unban elimina también los intentos fallidos asociados. Quien deba quedar libre de forma permanente va en ignoreip, seguido de fail2ban-client reload.
Convivencia con UFW y nftables
Aquí surge el daño más difícil de notar, porque fail2ban sigue informando alegremente de bloqueos mientras los paquetes pasan sin problema.
UFW es un frontend de iptables. Al arrancar o al recargar reescribe la tabla de filtrado con iptables-restore. En el proceso, las chains creadas por fail2ban desaparecen sin sustituto, y fail2ban no las vuelve a crear por sí solo. Por eso, después de cada ufw enable, ufw disable o ufw reload toca lanzar systemctl restart fail2ban. Hasta entonces, en fail2ban-client status sshd hay una larga lista de IP bloqueadas de las que ninguna está realmente bloqueada.
Es más limpio dejar que fail2ban bloquee directamente a través de UFW. La acción está en /etc/fail2ban/action.d/ufw.conf y viene incluida en las cuatro distribuciones. En jail.local:
[DEFAULT]
banaction = ufw
banaction_allports = ufw
Así, los bloqueos acaban como reglas dentro del propio UFW y sobreviven a sus recargas. Comprobación:
ufw status numbered
Las direcciones bloqueadas aparecen ahí como DENY IN, bastante arriba en la lista. Es importante que las reglas de bloqueo estén por delante de tu regla de apertura para el puerto 22, porque si no, la apertura actúa primero. Por eso la acción incluida las coloca al principio mediante insert.
Quien no use UFW y en su lugar mantenga un conjunto de reglas propio en /etc/nftables.conf tiene un problema emparentado. Un flush ruleset al principio de ese archivo, tal como propone la plantilla estándar, borra también f2b-table al recargar. O bien renuncias al flush global y vacías solo tu propia tabla, o bien encadenas el reinicio de fail2ban al servicio nftables. La ventaja de la tabla propia de fail2ban es que actúa con prioridad filter - 1, por delante de la tabla de filtrado normal y, por tanto, también por delante de las reglas de iptables-nft.
Lo que no deberías mezclar es banaction = nftables con un conjunto de reglas propio de iptables esperando que iptables -L muestre los bloqueos. Ambos caminos funcionan en paralelo, pero cada herramienta muestra únicamente su propio conjunto de reglas. Comprueba siempre con la herramienta que corresponde a la banaction configurada.
Mensajes de error literales y qué hay detrás
Los logs los encuentras, según el sistema, en dos sitios:
journalctl -u fail2ban --no-pager -n 100
tail -n 100 /var/log/fail2ban.log
Failed during configuration: Have not found any log file for sshd jail
El mensaje característico de Debian 12. El jail trabaja con backend = auto, busca /var/log/auth.log, y el archivo no existe porque no hay rsyslog instalado y todo acaba en el journal. El servicio ni siquiera arranca, y systemctl status fail2ban muestra Failed with result 'exit-code'. Dos soluciones: o bien apt install -y rsyslog y reiniciar una vez, o bien poner en jail.local, bajo [sshd], la línea backend = systemd. El segundo camino es el más moderno, pero exige el siguiente paquete.
Backend 'systemd' failed to initialize due to No module named 'systemd'
Has puesto backend = systemd, pero falta el enlace de Python con el journal. En Debian 12, python3-systemd solo está recomendado y no se arrastra en instalaciones sin paquetes recomendados. Solución:
apt install -y python3-systemd
systemctl restart fail2ban
Failed to access socket path: /var/run/fail2ban/fail2ban.sock. Is fail2ban running?
El cliente no alcanza al servidor. En la inmensa mayoría de los casos, el servicio simplemente no está en marcha porque ha fallado por un error de configuración. Lee primero systemctl status fail2ban y después el journal. En raras ocasiones, tras una interrupción brusca queda un archivo de socket huérfano, y entonces el arranque del servidor informa Server already running. En ese caso, elimina el archivo de socket y reinicia.
NOK: ('sshd',)
La respuesta a fail2ban-client status sshd cuando ese jail no existe en tiempo de ejecución. O falta enabled = true, o te has equivocado al escribir el nombre, o un error de sintaxis más arriba en jail.local se ha tragado la sección. Qué jails están realmente en marcha lo muestra fail2ban-client status. Y lo que fail2ban ha compuesto a partir de todos los archivos lo muestra el volcado de configuración:
fail2ban-client -d
Error banning y iptables not found
El jail cuenta correctamente, pero el bloqueo falla en la acción. Es típico en instalaciones ligeras de Debian 12, donde solo está nftables mientras el valor predeterminado es iptables-multiport. O instalas después apt install -y iptables, o cambias en jail.local a banaction = nftables. El segundo camino es el que Debian 13 y Ubuntu 24.04 siguen por sí mismos de todos modos.
Cuando ya nada ayuda: la marcha atrás ordenada
Como todos los ajustes están en jail.local, el camino de vuelta es corto. Aparta el archivo, reinicia el servicio y vuelves a estar con los valores del paquete:
mv /etc/fail2ban/jail.local /root/jail.local.bak
systemctl restart fail2ban
fail2ban-client status
Si así funciona, el fallo está en tu configuración, y vas reinsertando los bloques uno a uno. Si tampoco funciona así, la causa está en el entorno, es decir, en la fuente de logs que falta o en la herramienta de firewall que falta. Justo por eso jail.conf se queda intacto: esta marcha atrás en diez segundos solo es posible mientras el archivo del paquete siga en su estado original.
Para terminar, la valoración de conjunto. fail2ban reduce el ruido en los logs y detiene los intentos de inicio de sesión lentos y repetidos. Un ataque serio desde una botnet grande usa cada dirección una sola vez y pasa por delante de cualquier limitación de tasa. El paso más eficaz contra la fuerza bruta en SSH sigue siendo desactivar la autenticación por contraseña en favor de las claves. Y contra los ataques volumétricos solo ayuda el filtrado en la red, en nuestro caso mediante 3,2 Tbps de filtrado Arbor en tiempo real directamente en el centro de datos de Frankfurt am Main y hasta 17 Tbps de capacidad global de filtrado en las tarifas Professional. fail2ban es la capa de debajo, en el host, y ahí cumple su función de forma fiable en cuanto encajan tres cosas: la fuente de logs correcta, la acción de firewall correcta y una jail.local que, en caso de duda, puedas volver a quitar de una sola vez.
Preguntas frecuentes
¿De verdad tengo que dejar jail.conf sin tocar?
¿Por qué fail2ban no arranca en Debian 12 después de la instalación?
¿Qué significa exactamente backend = auto?
¿Cómo levanto un bloqueo?
¿Por qué fail2ban bloquea pero el atacante sigue entrando?
¿Cómo sé que fail2ban funciona de verdad?
¿Sustituye fail2ban a una protección DDoS?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

