
🌐 También en: English · Deutsch · Français
Deep Dive de la serie en torno al Ansible Tour. Nivel: administración Linux avanzada. El Tour explica los roles; los deep dives explican la tecnología que hay debajo – cómo funciona, por qué está aquí, qué hace exactamente esta instalación con ella, cómo se opera en el día a día y qué otras opciones habría habido.
fail2ban parece la herramienta más sencilla de estos servidores: leer un log, contar fallos, banear la dirección. En un host K3s cada parte de esa frase tiene trampa. Los logs viven en ficheros de contenedores que cambian de nombre en cada reinicio; los baneos tienen que acabar en un cortafuegos en el que también escriben otros programas; y una familia de direcciones que nadie probó puede convertirlo todo en un placebo. Este artículo sigue una sola petición maliciosa desde la línea de log hasta la entrada en el kernel, explica lo que la parte 5 de la visita solo roza – y describe tres casos en los que fail2ban decía «Ban» y el cortafuegos dejaba pasar la dirección igualmente.
🧭 Cómo funciona fail2ban
Jails, filtros, acciones
Una jail une tres cosas: un log que vigilar, un filtro que reconoce una línea mala y una acción que banea. El filtro es un conjunto de expresiones regulares (failregex, con ignoreregex para excepciones) en las que <HOST> marca la dirección. Cada coincidencia es un fallo de esa dirección. Si una dirección acumula maxretry fallos dentro de findtime, se ejecuta la acción y la banea durante bantime.
[nginx-k3s-wp-login]
enabled = true
filter = nginx-k3s-wp-login
logpath = /var/log/pods/ingress-nginx_ingress-nginx-*/nginx-ingress/*.log
backend = polling
maxretry = 10
findtime = 600Backends: cómo se lee el log
- systemd lee directamente el journal (filtrado por unidad) – robusto, sin ficheros ni rotación. Aquí para
sshd. - polling comprueba el fichero cada pocos segundos. Sencillo, funciona en cualquier sistema de ficheros – y la única opción cuando la ruta es un glob que cambia.
- pyinotify reacciona de inmediato a eventos de ficheros; más rápido, pero vigila los ficheros que existían al arrancar la jail.
La marca de tiempo importa tanto como la dirección: fail2ban lee la fecha de cada línea (datepattern) e ignora las líneas más antiguas que findtime. Un filtro con un formato de fecha equivocado no coincide con nada – en silencio.
La base de datos y recidive
fail2ban guarda los baneos actuales en una base SQLite (/var/lib/fail2ban/fail2ban.sqlite3). Tras un reinicio vuelve a aplicar todos los baneos que no han caducado. La jail recidive vuelve fail2ban contra sí mismo: su log es fail2ban.log, su filtro cuenta líneas «Ban», y quien es baneado tres veces en un día queda baneado 30 días en todos los puertos.
🛡️ Por qué está aquí
- Protección barata contra ataques automatizados a SSH, al inicio de sesión de WordPress, al de Nextcloud y a Grafana – y contra escáneres que recorren listas de rutas vulnerables conocidas.
- Baneos en el kernel, no en la aplicación. Una dirección baneada no llega a nada, ni siquiera al handshake de TLS.
- Visibilidad. Quién llama, cuántas veces y a qué puerta queda en el log de baneos – que se puede buscar en Loki desde octubre de 2026.
🔧 Qué hace este repositorio
Siete jails, dos fuentes de logs
| Jail | Log | Regla | Baneo |
|---|---|---|---|
| sshd | journal (backend systemd) | 3 fallos | por defecto 1 h |
| nginx-k3s-wp-login | log del ingress | 10 POST a /wp-login.php en 10 min | 1 h |
| nginx-k3s-nc-login | log del ingress | 10 POST a /login de Nextcloud en 10 min | 1 h |
| nginx-k3s-grafana-login | log del ingress | 5 inicios de sesión fallidos en Grafana (HTTP 401) en 10 min | 1 h |
| nginx-k3s-scanner | log del ingress | 20 respuestas 403 o 404 en 1 min | 1 h |
| nginx-k3s-bad-paths | log del ingress | 2 peticiones a rutas que aquí nunca existen (/.env, /.git/, /phpmyadmin, web shells …) en 1 h | 24 h |
| recidive | fail2ban.log | baneado 3 veces en un día | 30 días |
Todas las jails HTTP leen el log de acceso del controlador ingress, el único sitio por el que pasa cada petición, tanto del blog como de Nextcloud. Los filtros esperan el prefijo de containerd delante de cada línea (2026-06-19T14:24:11Z stdout F <cliente> - - [hora] "POST /wp-login.php …"); un filtro escrito para un log normal de nginx nunca coincidiría.
El baneo va directo a nftables
fail2ban no tiene ni tabla ni cadena propias. Su acción añade la dirección a un set que el cortafuegos comprueba al principio de su cadena input – cada jail en su propio par f2b_<jail>4/6 (el porqué, en el quinto fallo más abajo):
[Definition]
actionban = nft add element inet filter <nftset> { <ip> timeout <bantime>s }
actionunban = nft delete element inet filter <nftset> { <ip> } 2>/dev/null; true
[Init]
setname = banned
nftset = <setname>4
[Init?family=inet6]
nftset = <setname>6
# jail.local - every jail names its own set pair
[sshd]
action = nftables-banned-sets[setname=f2b_sshd]Cada elemento lleva su propia caducidad, así que un baneo caduca en el kernel aunque fail2ban haya muerto mientras tanto. El desbaneo es solo una limpieza en la medida de lo posible.
El fichero de log que se mueve
El log del ingress no es un único fichero. Es /var/log/pods/<namespace>_<pod>_<uid>/nginx-ingress/N.log, y cambian tanto el directorio del pod como el número: un reinicio del contenedor escribe 1.log después de 0.log, un pod nuevo recibe un directorio nuevo. fail2ban expande el glob de logpath una sola vez, al arrancar la jail. Un pequeño temporizador (fail2ban-ingress-logwatch, cada dos minutos) compara la lista actual de ficheros con la anterior y recarga las jails nginx-k3s-* cuando cambia. Sin él: ver el primer fallo de abajo.
🐛 Tres veces fail2ban dijo «Ban» y no pasó nada
1. Vigilar un fichero muerto durante días
El 23 de septiembre de 2026 resultó que las jails HTTP de los dos servidores Nextcloud no habían baneado nada en días. Tras un reinicio, fail2ban había arrancado antes que el contenedor del ingress, y tras un reinicio del contenedor las jails seguían leyendo un fichero al que ya no llegaban líneas. Todo parecía sano – el servicio corría, las jails aparecían en la lista, ningún error en ninguna parte. El temporizador descrito arriba es la solución. También es una de las razones por las que Monit comprueba ahora que cada jail esperada está activa (la historia del perro guardián empezó con un fail2ban caído en silencio durante dos días).
2. Una recarga del cortafuegos que vaciaba los sets
El cortafuegos sustituye su propia tabla en cada recarga – y con ella los sets, vacíos. fail2ban mantenía sus baneos en su base de datos y los creía activos. El 30 de septiembre de 2026, tras una ejecución del playbook, 15 de 17 baneos en uno de los servidores y 17 de 24 en el otro solo existían ya en fail2ban. El handler del cortafuegos reinicia ahora fail2ban justo después de cada recarga, y fail2ban vuelve a aplicar todos los baneos no caducados.
3. Baneos IPv6 que nunca fueron posibles
Hasta el 14 de octubre de 2026, cada jail pasaba nftset = banned4 a la acción – también para direcciones IPv6. nft se negaba a meter una dirección IPv6 en un set IPv4 («Address family for hostname not supported»), fail2ban registraba el baneo de todos modos y la dirección nunca se bloqueaba. En los tres servidores. La sección [Init?family=inet6] de arriba elige el set según la familia de direcciones; en la semana hasta el 20 de octubre de 2026, 15 de 444 baneos fueron a direcciones IPv6 – pocos, pero cada una de ellas habría entrado tranquilamente antes.
Un cuarto, encontrado por Loki
Cuando los logs empezaron a ir a Loki el 19 de octubre de 2026, la nueva alerta de errores de fail2ban saltó de inmediato. La mayoría eran inofensivos (KeyError sobre un log de pod recién borrado – el backend de sondeo pierde un fichero, la jail sigue). Pero el 13 y el 14 de octubre hubo errores reales: Failed to execute ban jail 'nginx-k3s-bad-paths' action 'nftables…' – baneos que no se produjeron mientras se rehacía la recarga del cortafuegos. Nadie los había visto, porque fail2ban no envía por correo sus propios errores. La alerta lo hace ahora, salvo el patrón inofensivo conocido.
Un quinto: un solo set para todas las jails
Hasta el 21 de octubre de 2026, todas las jails escribían en el mismo par de sets banned4/6. Parece ordenado y tiene una trampa: una dirección baneada por dos jails solo existe una vez en el set. Cuando terminaba el baneo más corto – el de la jail de escáneres al cabo de un día, por ejemplo –, la dirección salía del set, aunque recidive todavía quería mantenerla fuera 30 días. fail2ban la listaba como baneada; el kernel ya no. Una comparación ese día encontró entre una y cinco direcciones así por servidor. Desde entonces cada jail tiene su propio par (f2b_sshd4, f2b_recidive4, …) y solo añade y quita ahí, y un check de Monit (f2b_sets) compara con regularidad la lista de fail2ban con los sets.
📊 Qué banea
Una semana, del 14 al 20 de octubre de 2026, los tres servidores:
| Jail | Blog | Nextcloud A | Nextcloud B |
|---|---|---|---|
| nginx-k3s-bad-paths | 107 | 74 | 74 |
| nginx-k3s-scanner | 63 | 50 | 47 |
| sshd | 9 | 2 | 11 |
| recidive | 4 | 2 | – |
| nginx-k3s-wp-login | 1 | – | – |
| Porcentaje IPv6 | 11 de 184 | 2 de 128 | 2 de 132 |
El panorama es claro: casi todos los baneos vienen de escáneres que buscan ficheros .env, directorios Git, paneles de administración y web shells. Los ataques reales al inicio de sesión son raros – el día del análisis, el login de WordPress recibió dos POST en tres horas, y desde el 19 de octubre de 2026 /wp-login.php solo responde de todos modos a direcciones de la lista de países. Hay pocos baneos de SSH porque el puerto 10022 solo acepta rangos de direcciones alemanes; lo que queda son algunas direcciones de dentro de ese rango.
🧰 Administración en el día a día
# todas las jails, y luego una en detalle (baneadas ahora, total)
fail2ban-client status
fail2ban-client status nginx-k3s-bad-paths
# la visión del cortafuegos: cada jail tiene su propio par de sets f2b_<jail>4/6
nft list set inet filter f2b_bad_paths4
nft list set inet filter f2b_recidive4
fail2ban-client get sshd action nftables-banned-sets setname # -> f2b_sshd
# levantar un baneo (ambas cosas: el registro de fail2ban y el set del kernel)
fail2ban-client set nginx-k3s-scanner unbanip 203.0.113.7
# ¿coincide un filtro con líneas reales? (contra el log actual del ingress)
fail2ban-regex /var/log/pods/ingress-nginx_ingress-nginx-*/nginx-ingress/0.log \
/etc/fail2ban/filter.d/nginx-k3s-bad-paths.conf
# ¿qué fichero vigila ahora una jail?
fail2ban-client get nginx-k3s-wp-login logpathCuando una jail no salta
- ¿Vigila el fichero correcto?
get <jail> logpathfrente als /var/log/pods/…– tras reiniciar el controlador ingress, el temporizador debería haberla recargado en dos minutos. - ¿Coincide el filtro?
fail2ban-regexcontra el fichero real. Un cambio de formato de línea tras actualizar el ingress aparece aquí. - ¿Se entiende la fecha? Las líneas más antiguas que
findtimese ignoran; undatepatternequivocado vuelve «antigua» cada línea. - ¿Llega el baneo al kernel? Comparar
statusconnft list set. Si difieren, buscar «Failed to execute ban» enfail2ban.log– o en Loki:{job="fail2ban"} |= "ERROR".
No probar desde la propia máquina de salto
Pedir una vez /.env para ver si funciona la jail de bad-paths es tentador. Dos peticiones en una hora banean la dirección durante 24 horas – incluida la máquina de salto, y con ella todas las sesiones SSH a ese servidor. Los rangos privados están en ignoreip; la dirección pública de la red de administración, no. Mejor probar con fail2ban-regex contra una línea de log, o desde una dirección de la que se pueda prescindir un día.
🆘 Chuleta: síntoma → comando
La versión corta de este artículo para el momento en que algo va mal. Ejecutar en el host como root; <…> son marcadores.
| Síntoma | Comando |
|---|---|
| Una jail no banea nada | fail2ban-client status <jail> |
| El filtro no coincide | fail2ban-regex <logfile> /etc/fail2ban/filter.d/<filter>.conf |
| Baneada, pero sigue entrando | nft get element inet filter f2b_<jail>4 '{ <ip> }' # "No such file" = not in the set |
| ¿En qué set escribe una jail? | fail2ban-client get <jail> action nftables-banned-sets setname |
| Levantar un baneo | fail2ban-client unban <ip> |
| ¿Ha fallado un baneo? | grep -E "ERROR|Failed to execute" /var/log/fail2ban.log | tail |
| fail2ban parado, o falta una jail | systemctl status fail2ban |
| Sets vacíos tras recargar el cortafuegos | systemctl restart fail2ban # re-applies every active ban |
| Te has bloqueado a ti mismo | # from another address or the provider console: |
🔭 Lo que viene
- Baneos en Grafana – hecho. Desde el 21 de octubre de 2026 el repositorio incluye un panel «fail2ban – Bans»: baneos por jail y hora, los totales del periodo elegido y las últimas líneas de baneo, todo a partir de las líneas que Loki ya tiene.
- Una lista de baneo para los tres servidores. Un escáner baneado en el blog suele ser el mismo que prueba los servidores Nextcloud una hora después. Compartirla requeriría una pequeña sincronización entre los hosts.
- Listas de reputación (al estilo de CrowdSec) como capa adicional – al precio de un servicio externo.
⚖️ Alternativas – y por qué fail2ban
| Opción | Punto fuerte | Por qué no aquí |
|---|---|---|
| CrowdSec | Listas de bloqueo compartidas por la comunidad, escenarios, bouncers | Más piezas móviles (agente, API local, bouncer) y datos compartidos con un servicio externo. |
| sshguard | Pequeño, rápido, centrado en SSH | Cubre bien SSH, peor las jails HTTP. |
| Solo límites de tasa en el ingress | Protege HTTP sin demonio adicional | Frena a los atacantes pero nunca los banea, y no hace nada por SSH. Se usa además (límite de la búsqueda). |
| fail2ban | Funciona con cualquier log, acciones programables, maduro | Elegido – con las tres lecciones de arriba incorporadas. |
fail2ban es antiguo, sencillo y hace exactamente lo que promete. El trabajo nunca fue la herramienta; fue asegurarse de que cada «Ban» de su log acabara también como una entrada en el kernel. Desde octubre de 2026 eso se comprueba – con el handler del cortafuegos, con Monit y con una alerta de logs – en lugar de suponerse.


