Configurar fail2ban y bloquear ataques de fuerza bruta de forma automática

Publicado el 16 min de lectura

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:

  1. jail.conf
  2. jail.d/*.conf en orden alfabético
  3. jail.local
  4. jail.d/*.local en 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
Sistemafail2banContenido de defaults-debian.conf
Debian 131.1.0banaction = nftables, banaction_allports = nftables[type=allports], y para [sshd] además backend = systemd, journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd y enabled = true
Debian 121.0.2solo [sshd] y enabled = true
Ubuntu 24.041.0.2banaction = nftables, banaction_allports = nftables[type=allports], backend = systemd, además de [sshd] con enabled = true
Ubuntu 22.040.11.2solo [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:

Sistemabanaction de fábricaComprobación del bloqueo activo
Debian 13nftablesnft list table inet f2b-table
Ubuntu 24.04nftablesnft list table inet f2b-table
Debian 12iptables-multiportiptables -n -L f2b-sshd
Ubuntu 22.04iptables-multiportiptables -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?
Sí. El archivo pertenece al paquete y puede quedar reemplazado en las actualizaciones. Todos los ajustes van a /etc/fail2ban/jail.local. Ese archivo se lee después de jail.conf y de jail.d/*.conf, así que también sobrescribe los valores predeterminados de defaults-debian.conf.
¿Por qué fail2ban no arranca en Debian 12 después de la instalación?
Porque el jail sshd trabaja con backend = auto y espera /var/log/auth.log, que no existe si rsyslog no está instalado. En el journal aparece entonces "Failed during configuration: Have not found any log file for sshd jail". Solución: o instalar rsyslog, o poner backend = systemd e instalar después python3-systemd.
¿Qué significa exactamente backend = auto?
auto prueba por orden pyinotify y polling. Ambos son métodos basados en archivos. auto nunca elige el journal de systemd. Quien quiera analizar el journal tiene que poner backend = systemd de forma explícita.
¿Cómo levanto un bloqueo?
De forma individual con fail2ban-client set sshd unbanip DIRECCION-IP, en todos los jails a la vez con fail2ban-client unban DIRECCION-IP y, en caso de emergencia, todo de golpe con fail2ban-client unban --all. Quien deba quedar libre de forma permanente va en ignoreip, seguido de fail2ban-client reload.
¿Por qué fail2ban bloquea pero el atacante sigue entrando?
Lo más habitual es que UFW haya reescrito entretanto la tabla de filtrado con iptables-restore y haya eliminado las chains de fail2ban. Después de cada ufw enable, disable o reload hay que reiniciar fail2ban. Más limpio es banaction = ufw, porque así los bloqueos viven dentro del propio UFW.
¿Cómo sé que fail2ban funciona de verdad?
No por el hecho de que el servicio esté en marcha. Comprueba con fail2ban-regex si el filtro encuentra líneas y bloquea a mano, a modo de prueba, una dirección de 203.0.113.0/24. Después tiene que aparecer en el firewall, y de forma acorde a la banaction configurada: en Debian 13 y Ubuntu 24.04 con nft list table inet f2b-table dentro del set addr-set-sshd, y en Debian 12 y Ubuntu 22.04 con iptables -n -L f2b-sshd en la cadena del mismo nombre. En los sistemas con nftables no existe ninguna cadena f2b-sshd.
¿Sustituye fail2ban a una protección DDoS?
No. fail2ban actúa en el host contra los intentos de inicio de sesión repetidos de direcciones concretas. Los ataques volumétricos tienen que filtrarse en la red, antes de que lleguen al servidor.

fail2ban SSH Seguridad de servidores Fuerza bruta Debian Ubuntu nftables UFW