
Si tienes un VPS o un servidor en casa expuesto a internet, esto te va a sonar. Abres los puertos 80 y 443 con servicios como Nextcloud, Jellyfin o Home Assistant, y en cuestión de minutos empieza la fiesta. Pero no es una fiesta de bienvenida: los bots de Rusia, China, Vietnam y medio mundo llaman a tu puerta. Escanean puertos, prueban rutas, buscan archivos php, xmlrpc, wp-admin, cualquier rendija por la que colarse. Has puesto un cartel de bienvenido en la entrada de tu casa y han llegado todos los vecinos del bloque, incluidos los que no invitaste. No son pocos. En las últimas 24 horas, mi VPS recibió 921.000 peticiones y bloqueé 650.000. El 70% del tráfico era basura. Por eso nació Shuul.
El problema: tu VPS no es solo tuyo
Tener un servidor accesible desde internet es una pasada, pero también un quebradero de cabeza. Yo tengo en mi VPS tres páginas web y, desde hace un tiempo, también el feed de sospechosos habituales. Hasta que el feed llegó, tenía el servidor configurado para que solo fuera accesible desde España, con algunas restricciones adicionales. Todo más o menos controlado. Pero el feed debía poder consultarse desde cualquier lugar del mundo, y ahí empezó el problema.
Porque el tráfico que llega a un servidor rara vez es de personas interesadas en tu contenido. La mayor parte del tiempo, las visitas no son de seres humanos. Son bots. Bots que escanean, bots que prueban vulnerabilidades, bots que buscan configuraciones mal ajustadas, paneles de administración olvidados, archivos de respaldo colgados en lugares públicos.
Y no te creas que esto solo le pasa a los grandes servidores. Da igual que tengas una página web modesta con cincuenta visitas al día. Los bots no discriminan. De hecho, cuanto más pequeño es el servidor, menos monitorización tienes, y más fácil les resulta pasar desapercibidos.
En las últimas 24 horas antes de grabar este episodio, Shuul registró 921.000 peticiones entrantes. De esas, permitió 270.000 y bloqueó 650.000, un 70% del tráfico. De esos bloqueos, 205.797 corresponden únicamente a filtros por ubicación geográfica. El resto son bloqueos por patrones, por rate limiting, por intentos de ataque.
Por qué las soluciones tradicionales se quedan cortas
Cuando empiezas a buscar soluciones para proteger tu VPS, te encuentras con las típicas: Fail2ban, listas blancas, listas negras, geolocalización en Traefik, plugins de seguridad. Pero ninguna termina de resolver todas las piezas del puzle.
Fail2ban es la solución clásica, un clásico de 2004 que sigue vigente. Escanea logs, detecta patrones de acceso fallido y banea IPs con iptables. Pero tiene limitaciones importantes. Es un proceso en Python que parsea logs, lo que consume recursos y añade latencia. No protege contra ataques distribuidos (un atacante con muchas IPs rotativas lo esquiva fácilmente). Y trabaja a nivel de firewall de red, no a nivel de aplicación HTTP. No entiende de paths, de métodos, de headers, de códigos de estado.
Por otro lado, Traefik tiene su propio sistema de middlewares para filtrar por geolocalización, para hacer listas blancas o negras. Pero es limitado. No es tan granular como me gustaría.
Y luego está CrowdSec, una evolución moderna de Fail2ban con inteligencia colectiva. 250.000 watchers activos, 40 millones de señales al día. Pero tampoco cubría lo que yo necesitaba.
El problema es el mismo de siempre. Cuando buscas algo muy concreto, las soluciones generalistas no encajan. Necesitaba que una URL específica (el feed de sospechosos habituales) fuera accesible desde cualquier lugar del mundo pasara lo que pasara. Y al mismo tiempo, quería que el resto de servicios se comportaran de manera distinta según la ubicación del visitante. Que alguien de Estados Unidos intentara ver una página concreta, me daba igual. Pero que intentara acceder a rutas administrativas, no.
Ahí es donde entra Shuul.
Así nace Shuul, el guardián de tus datos
Shuul ya lo mencioné hace algunos episodios, cuando era un proyecto más incipiente. Ahora ha madurado. Nació precisamente en el momento en que el feed de sospechosos habituales entró dentro de mi VPS. De repente, necesitaba una capa de protección unificada, algo que me permitiera filtrar todo lo que llega y hacerlo a mi manera.
La premisa era clara: quería un servicio que actuara como forward auth de Traefik, que decidiera en tiempo real si una petición pasaba o se quedaba fuera, y que además incorporara un sistema de rate limiting post-factum, después de que el backend respondiera. Algo parecido a Fail2ban, pero más granular, sin parsear logs, y funcionando a nivel HTTP.
Y todo esto, encapsulado en un binario estático de 20 megas.
La arquitectura de Shuul: Rust, React y un poco de Go
Shuul está construido con tres piezas bien diferenciadas.
El backend está escrito en Rust, utilizando el framework Axum. Es rápido, seguro, con un consumo de recursos mínimo. Rust me da la tranquilidad de que no voy a tener fugas de memoria ni problemas de concurrencia. Compilado a un binario estático de unos 20 megas, sin dependencias externas.
El frontend está desarrollado en React con TypeScript, utilizando Vite como bundler y shadcn/ui para los componentes. Es una SPA (Single Page Application) que se sirve desde el propio backend, con un panel de control completo.
La base de datos es SQLite. Inicialmente empecé con PostgreSQL, pero me di cuenta de que no tenía sentido. Toda esta información (logs, estadísticas, baneos) la puedo volcar a SQLite sin necesidad de un servidor de base de datos externo. Menos complejidad, menos recursos, más sencillez.
Y luego hay un pequeño módulo en Go que es el plugin de Traefik, el Shuul Reporter. Este plugin resuelve un problema fundamental: Shuul como ForwardAuth solo ve las peticiones entrantes, nunca ve la respuesta del backend. Si un atacante pasa el filtro WAF pero el backend responde con un 401 (login incorrecto) o un 404 (ruta inexistente), Shuul no lo sabe. El plugin en Go wrappea el http.ResponseWriter para interceptar el código de estado y lo reporta a Shuul de forma asíncrona, sin afectar a la latencia de la respuesta.
El despliegue se hace con Docker multi-etapa. Una imagen pequeña, rápida, segura y sin sorpresas de memoria.
El pipeline WAF: la primera línea de defensa
El funcionamiento de Shuul se basa en dos pipelines independientes. El primero es el pipeline WAF.
Cuando una petición llega a Traefik, éste se la envía al endpoint POST /api/v1/shuul mediante el middleware forwardAuth. Shuul extrae información de la petición: la IP de origen, el protocolo, el host, la URI, el User-Agent, el método HTTP, los headers. A partir de la IP, consulta la base de datos de geolocalización MaxMind GeoLite2 para obtener el país y la ciudad de origen.
El caché. Los bots bombardean con muchas IPs distintas, y consultar MaxMind para cada petición sería una locura. Por eso Shuul utiliza una caché LRU con moka de 10.000 entradas y un TTL de una hora. Las IPs repetidas dentro de esa ventana no vuelven a consultar la base de datos.
Una vez que tiene toda la información, evalúa las reglas configuradas. Cada regla tiene un peso (weight). Las reglas con menor peso se evalúan primero. La primera que matchea es la que gana. El resultado puede ser Allow (permite el paso, devuelve 200 OK) o Deny (bloquea, devuelve 403 Forbidden).
Las reglas pueden filtrar por 14 campos distintos: IP, FQDN, path, query, país, user-agent, método, referer, content-type, accept-language, protocolo, ciudad, código de país. Y admiten patrones regex para matching flexible.
Tengo reglas con pesos muy bajos para las excepciones: la IP de mi casa, localhost, las IPs de los suscriptores del feed de sospechosos habituales. Esas devuelven 200 directamente, sin más comprobaciones. Luego vienen las reglas con pesos altos: 1000 para bloqueo por ubicación geográfica, 1010 para bloqueo de nodos de salida de Tor. Entre medias, todo un abanico de reglas para casos concretos.
Además, cada regla puede funcionar en tres modos:
- Enforce: bloquea si la regla no permite explícitamente.
- Log only: registra pero no bloquea. Perfecto para probar reglas nuevas antes de activarlas.
- Off: ignora la regla por completo.
El pipeline Jail: rate limiting post-factum
El segundo pipeline es el Jail. Y aquí está la gracia del asunto.
El ForwardAuth solo decide si la petición entra o no entra. No sabe cuál ha sido la respuesta del backend. Un atacante puede pasar el filtro WAF (porque su IP no está bloqueada, porque la ruta que pide parece legítima) y sin embargo estar haciendo algo malicioso: probar contraseñas, escanear rutas que no existen, forzar endpoints.
Para eso está el Shuul Reporter, el plugin de Traefik en Go. Cuando el backend responde, el plugin captura el código de estado HTTP. Si el código es 401 (no autorizado), 403 (prohibido) o 404 (no encontrado), lo reporta a Shuul mediante el endpoint POST /api/v1/report. Los 2xx se ignoran. El reporte es fire-and-forget, asíncrono, sin impacto en la latencia.
Shuul recibe ese reporte y lo procesa contra los perfiles de rate limiting. Cada perfil define:
- Un número máximo de reintentos.
- Una ventana de tiempo (ventana deslizante por IP).
- Un tiempo de baneo.
- Los códigos HTTP que cuentan como fallo.
- Un escalado del tiempo de baneo: 1x, 2x, 4x, 8x.
De serie, Shuul incluye 9 perfiles de rate limiting. Algunos ejemplos:
| Perfil | Reintentos | Ventana | Baneo | Códigos |
|–|||-||
| Auth Brute Force | 5 | 300s | 900s | 401 |
| Admin Guard | 5 | 300s | 3600s | 401, 403 |
| Path Scanning | 20 | 60s | 300s | 403, 404 |
| API Abuse | 100 | 60s | 300s | 401, 403, 429 |
| Scanner Aggressive | 50 | 10s | 1800s | 403, 404, 405, 500 |
La implementación del rate limiting utiliza un ring buffer circular para comprobaciones en O(1), lo que permite manejar decenas de miles de peticiones por segundo sin despeinarse. Y el decaimiento del contador de baneos es configurable en días, para que las IPs que se portan bien no estén baneadas para siempre.
Templates: 81 reglas listas para usar
Configurar un WAF desde cero puede ser abrumador. Por eso Shuul incluye 81 templates preconfigurados en tres categorías.
Templates WAF para proteger contra:
- Inyección SQL
- Cross-site scripting (XSS)
- Path traversal
- Log4j y otras vulnerabilidades conocidas
- Escáneres genéricos
- Bots conocidos
Templates Jail para detectar:
- Escaneo de directorios
- Fuerza bruta en formularios de login
- Paneles de administración (WordPress, phpMyAdmin, Grafana, Portainer, etc.)
- APIs genéricas
- CMS (WordPress, Drupal, Joomla, Magento, PrestaShop)
- Cloud (Nextcloud, Home Assistant)
- Email (Roundcube, RainLoop, SnappyMail)
Templates de perfiles de rate limiting, los 9 que te he mencionado antes.
Cada template se puede aplicar directamente con un solo clic desde el panel de administración. Y por supuesto, puedes crear tus propias reglas desde cero.
El dashboard y los logs en tiempo real
De nada sirve tener un WAF si no puedes ver lo que está pasando. Por eso el frontend de Shuul incluye un panel de control completo.
La página principal muestra un resumen con el número de reglas activas, el total de peticiones procesadas, los baneos activos y un checklist de seguridad. Los gráficos muestran la evolución temporal con barras apiladas y líneas, rankings por países, por reglas de bloqueo, por métodos HTTP, por paths, por FQDNs. Todo en gráficos donut interactivos.
Pero lo que para mí es imprescindible son los logs en tiempo real. Puedes ver las peticiones que van llegando en vivo, filtradas por admitidas, baneadas, o las que están en match (peticiones que no coinciden con ninguna regla). El buffer circular es configurable, de 1.000 a 20.000 entradas. Con auto-refresh, modo oscuro y claro, y filtros por tipo de evento.
Tener los logs en tiempo real te permite detectar patrones al instante. Ves una IP de un país que no esperabas accediendo a rutas de administración y puedes bloquearla al momento. Sin esperar a que Fail2ban parseé los logs. Sin demoras.
Integración con OIDC y PocketID
Para el acceso al panel de administración, Shuul no utiliza usuario y contraseña tradicionales. En su lugar, usa OpenID Connect (OIDC) con PocketID como proveedor.
PocketID es otro de mis proyectos, un proveedor OIDC ligero para autohosting. La integración es sencilla: configuras las variables de entorno con la URL del issuer, el client ID, el client secret y la URL de redirect, y ya está. Sin gestionar usuarios, sin contraseñas que guardar, sin formularios de registro. Todo centralizado a través de SSO.
Las variables de entorno que necesita Shuul son muy sencillas:
OIDC_ISSUER_URL— la URL del proveedor OIDCOIDC_CLIENT_ID— el identificador de clienteOIDC_CLIENT_SECRET— el secretoOIDC_REDIRECT_URL— la URL de callback
Además, necesitas configurar la base de datos SQLite, el secreto para firmar los JWTs internos, la clave de MaxMind para la geolocalización, y poco más. Todo cabe en un archivo de entorno de menos de veinte líneas.
Cómo se integra con Traefik
La integración con Traefik es directa. En la configuración de Traefik, defines un middleware de tipo forwardAuth que apunte a Shuul:
http:
middlewares:
shuul-auth:
forwardAuth:
address: "http://shuul:3000/api/v1/shuul"
trustForwarders: true
Y luego aplicas ese middleware a los routers que quieras proteger. Traefik se encarga de enviar cada petición a Shuul antes de pasarla al backend. Si Shuul responde con un 200, la petición sigue su curso. Si responde con un 403, se bloquea.
Para el pipeline Jail, necesitas instalar el plugin traefik-shuul-reporter en Traefik y configurarlo para que reporte los códigos de estado a Shuul. Es un middleware que se coloca después del router pero antes del backend, capturando la respuesta.
El flujo completo es el siguiente:
- Llega una petición a Traefik.
- Traefik la envía al middleware forwardAuth que apunta a Shuul.
- Shuul evalúa el pipeline WAF: geolocalización, reglas, regex.
- Si la regla matchea con Allow, devuelve 200 OK.
- Si la regla matchea con Deny, devuelve 403 Forbidden.
- Si la petición pasa, Traefik la envía al backend.
- El backend responde.
- El plugin Shuul Reporter captura el código de estado.
- Si es un código no exitoso (>= 300), lo reporta a Shuul.
- Shuul procesa el reporte contra los perfiles de rate limiting.
- Si se excede el threshold, la IP queda baneada.
Todo esto ocurre en milisegundos.
Comparativa con alternativas
¿Dónde encaja Shuul respecto a lo que ya existe? Fail2ban es la opción más conocida, con 18.700 estrellas en GitHub y más de 6.000 commits. Sigue vigente: escanea logs, detecta patrones de acceso fallido y banea IPs con iptables. Pero consume recursos parseando logs, no entiende de HTTP, no protege contra ataques distribuidos, y cada servicio requiere su propia jail.
CrowdSec es más moderna, con inteligencia colectiva de 250.000 watchers activos y 40 millones de señales diarias compartidas. Bloquea hasta el 95% del tráfico malicioso. Pero no te permite afinar con el nivel de detalle que necesitas para un caso como el mío: permitir una URL concreta para todo el mundo mientras bloqueas por país en el resto. Para eso está Shuul.
Y una ventaja difícil de igualar: es un binario estático de 20 megas. Sin Python, sin dependencias, sin servidor de base de datos externo. docker compose up y ya está funcionando.
El despliegue con Docker Compose no tiene complicación. Un archivo de configuración con el servicio, las variables de entorno para OIDC y MaxMind, el volumen para SQLite, y tienes el WAF operativo en menos de cinco minutos. Si ya usas Traefik, la integración es coser y cantar: un middleware de tipo ForwardAuth y listo.
Conclusión
Los bots no descansan. Están ahí, las 24 horas del día, los 7 días de la semana, los 365 días del año, escaneando, probando, buscando incansablemente cualquier rendija por donde colarse. Pero con Shuul, yo sí descanso.
Construir tu propia herramienta de seguridad tiene algo especial. Igual que me pasó con Minerva, cuando buscas algo muy concreto y no lo encuentras, la opción de hacerlo tú mismo no solo es válida, sino que te obliga a enfrentarte a problemas con los que nunca te habías enfrentado antes. Aprendes sobre geolocalización, sobre rate limiting, sobre patrones de ataque, sobre cómo funciona HTTP a nivel de proxy. Y al final, acabas con una herramienta que entiendes al milímetro, que sabes cómo funciona por dentro, y que puedes adaptar a cualquier necesidad que surja.
Shuul es de código abierto, está en GitHub, y puedes usarlo, modificarlo, adaptarlo a tus necesidades. No es la solución para todo el mundo, pero si tienes un VPS con Traefik y quieres blindarlo contra bots, es una opción que puedes considerar.
Lo mejor de todo es que el código está ahí, en GitHub, con documentación y ejemplos de configuración. Puedes abrir issues, proponer mejoras, o simplemente copiar las partes que te interesen para tu propia configuración. Así es el software libre, y así debería ser siempre.
Después de todo, saber que el 70% del tráfico que iba a tu servidor era basura y que ya no llega, da una tranquilidad que no tiene precio.
Más información
- Shuul en GitHub — Repositorio principal del proyecto con documentación completa, API, arquitectura y guía de inicio rápido
- traefik-shuul-reporter en GitHub — Plugin de middleware para Traefik en Go que captura códigos de estado HTTP para rate limiting inteligente
- Traefik ForwardAuth Documentation — Documentación oficial del middleware ForwardAuth de Traefik
- MaxMind GeoLite2 — Base de datos gratuita de geolocalización IP, gratuita con registro
- Fail2Ban en GitHub — El clásico framework de prevención de intrusiones (18.7k estrellas)
- CrowdSec — Motor de seguridad open source con inteligencia colectiva y 250K+ watchers activos
- PocketID en GitHub — Proveedor OIDC ligero para autohosting, creado por el mismo autor de Shuul
- OpenID Connect — Protocolo de autenticación basado en OAuth 2.0
- Web Application Firewall (Wikipedia) — Definición, historia y modos de despliegue de los WAF