Cómo activar la detección de bots de CrowdSec 1.8
Índice de contenidos
- Puntos clave
- Qué hace la detección de bots de CrowdSec 1.8
- Qué necesitas antes de activarla
- Cómo montar el laboratorio con Docker Compose
- Cómo comprobar que el bouncer sirve el reto
- Qué ve CrowdSec cuando llega un navegador automatizado
- Cuánto tarda un visitante real en pasar el reto
- Un navegador que nunca termina el reto
- Cómo evitar que tu CSP congele el reto
- Cómo limitar el reto a las rutas que importan
- Qué pasa con Googlebot y los rastreadores de IA
- Cuándo el reto acaba en un bloqueo de IP
- Límites de una función en alfa
- Preguntas frecuentes
- ¿La detección de bots de CrowdSec sustituye a un captcha?
- ¿Bloquea a Googlebot y perjudica el SEO?
- ¿Funciona con Traefik?
- Conclusión
- Fuentes
CrowdSec 1.8 añade a su WAF una detección de bots, todavía en alfa, que sirve a cada visitante una página con prueba de trabajo y huella del navegador. Se activa con la colección appsec-bot-challenge y sus configuraciones en la adquisición AppSec, siempre detrás de un bouncer compatible como el de OpenResty 1.2.3.
CrowdSec 1.8 trae a su WAF una detección de bots que, antes de dejar pasar una petición, sirve una página con prueba de trabajo y huella del navegador. La función está en alfa y se nota en los bordes. Esta guía la activa con Docker detrás del bouncer de OpenResty, la prueba con curl, con Playwright y con un Chromium sin automatizar, y documenta las trampas que encontré por el camino: la CSP del sitio, una colección que falta y un navegador que espera para siempre.
Puntos clave
- La detección de bots llegó con CrowdSec 1.8.0 el 31 de agosto de 2026 y la documentación la marca como alfa; aquí se usa la 1.8.1, del 3 de septiembre.
- Se activa con la colección
crowdsecurity/appsec-bot-challengey el comodíncrowdsecurity/appsec-bot-*en la adquisición AppSec, y solo detrás de un bouncer compatible. - Un Chromium manejado por Playwright sacó 570 puntos frente a un umbral de 75; un Chromium sin automatizar pasó en 1,6 s de mediana, 0,8 s de ellos de espera fija.
- Si tu servidor web añade su propia Content-Security-Policy, la página del reto se congela; un
mapde nginx lo arregla. - Googlebot, GPTBot y el resto de rastreadores conocidos se libran del reto solo si su IP se verifica; un User-Agent falso desde otra IP recibe la página igual.
Qué hace la detección de bots de CrowdSec 1.8
La detección de bots es una fase nueva del componente AppSec (el WAF de CrowdSec) que pregunta qué es el cliente en lugar de qué envía. Cuando llega una petición sin la cookie __crowdsec_challenge, el bouncer no la pasa al origen. En su lugar devuelve una página que resuelve una prueba de trabajo y recoge una huella del dispositivo con fpscanner, la biblioteca de código abierto de Antoine Vastel. El navegador envía ambas cosas a /crowdsec-internal/challenge/submit y el componente AppSec decide.
La decisión es una suma de puntos. Cada señal de la huella añade un peso. Las marcas de automatización (cdp, webdriver, playwright o un User-Agent de bot) valen 100; los rasgos de navegador sin interfaz, 50, y las señales débiles como la zona horaria UTC, 30, 15 o 5. Si la suma llega al umbral, el envío se rechaza y queda una alerta; si no, el navegador recibe la cookie y sigue.
CrowdSec 1.8.0 salió el 31 de agosto de 2026, y la 1.8.1, que corrige un falso positivo con Brave y sus escudos activados, el 3 de septiembre. El proyecto tiene licencia MIT y su repositorio suma 14.866 estrellas. La documentación de la detección de bots[1] es clara sobre el estado de la función: "Bot detection is currently in alpha. It’s ready to try and we’d love your feedback, but the configuration, helpers and shipped rules may still change between releases." Por eso todo lo que sigue fija versiones.
Qué necesitas antes de activarla
Hacen falta cuatro piezas, y la tercera es la que menos se espera:
- Un componente AppSec que ya funcione. Si partes de cero, la guía para instalar CrowdSec como WAF comunitario cubre el agente, la API local y los bouncers.
- Un bouncer que entienda la remediación
challenge. La página de activación[2] avisa de que detrás de uno incompatible el resultado más probable es rechazar en silencio a todos los clientes. - Un anfitrión capaz de compilar WebAssembly: arm64, o amd64 con SSE4.1, y permiso para reservar memoria ejecutable. Si falta, CrowdSec no arranca y lo dice con
wasm compiler mode unavailable. - Clientes que ejecuten JavaScript y acepten cookies. Una API, un monitor o un lector de RSS no pasarán nunca el reto, así que hay que eximirlos.
El código del reto en la 1.8.1[3] explica el requisito de WebAssembly. El modo intérprete de wazero resultó "at least 60 times slower" al ofuscar el JavaScript, así que solo se admite el compilador. En mi máquina, un arm64 dentro de Docker, el entorno de ejecución arrancó sin quejas y tardó 2 s en cada inicio o recarga.
Estos son los bouncers que la documentación da por compatibles y lo que comprobé de cada uno:
| Bouncer | Desde qué versión | Qué comprobé |
|---|---|---|
| nginx y OpenResty (Lua) | 1.2.0, del 21 de julio de 2026, con la biblioteca Lua 1.0.15 | probado con crowdsecurity/openresty:v1.2.3 |
| Traefik (plugin de maxlerebourg) | versión preliminar 1.8.0-alpha, del 12 de septiembre de 2026 | sin probar |
| HAProxy SPOA y Envoy | listados como compatibles | sin probar |
La versión de la biblioteca Lua la deduje del código: la 1.0.14 no menciona la remediación challenge y la 1.0.15 sí. Si tu proxy es Traefik, como en la instalación de Traefik con Docker Compose, el reto exige esa versión preliminar del plugin 1.8.0-alpha[4].
Cómo montar el laboratorio con Docker Compose
Monté tres contenedores en una red propia: CrowdSec 1.8.1, el bouncer de OpenResty 1.2.3 y traefik/whoami como origen que devuelve la petición recibida. Todo corrió el 16 de septiembre de 2026 en un devcontainer linux/arm64 con 18 núcleos. La primera mitad de compose.yaml declara CrowdSec con sus colecciones y la clave del bouncer:
services:
crowdsec:
image: crowdsecurity/crowdsec:v1.8.1
environment:
COLLECTIONS: >-
crowdsecurity/appsec-virtual-patching
crowdsecurity/appsec-generic-rules
crowdsecurity/appsec-bot-challenge
BOUNCER_KEY_openresty: clave_del_bouncer_de_laboratorio
volumes:
- ./appsec.yaml:/etc/crowdsec/acquis.d/appsec.yaml:ro
- cs-data:/var/lib/crowdsec/data
- cs-config:/etc/crowdsec
La variable COLLECTIONS instala tres colecciones al arrancar, y la segunda no es opcional. En el primer intento solo puse la de parches virtuales y la del reto, y CrowdSec se detuvo con un error fatal al cargar la configuración appsec-default:
time="2026-09-16T14:07:44Z" level=fatal msg="crowdsec init: while
loading acquisition config: /etc/crowdsec/acquis.d/appsec.yaml:
datasource of type appsec: unable to build appsec_config: unable to
load outofband rule crowdsecurity/experimental-* : no appsec-rules
found for pattern crowdsecurity/experimental-*"
appsec-default carga las reglas crowdsecurity/generic-* y crowdsecurity/experimental-*, que vienen en appsec-generic-rules. La guía rápida de AppSec instala las dos colecciones; la página del reto da por hecho que ya las tienes.
La segunda mitad, que va bajo el mismo services:, levanta OpenResty con la URL de AppSec y el origen de prueba. Los tres tiempos de espera van explícitos porque la plantilla de configuración de la imagen los trae vacíos:
services:
# aquí va el servicio crowdsec del bloque anterior
openresty:
image: crowdsecurity/openresty:v1.2.3
depends_on: [crowdsec, whoami]
environment:
API_URL: http://crowdsec:8080
API_KEY: clave_del_bouncer_de_laboratorio
APPSEC_URL: http://crowdsec:7422
APPSEC_CONNECT_TIMEOUT: "100"
APPSEC_SEND_TIMEOUT: "100"
APPSEC_PROCESS_TIMEOUT: "1000"
volumes:
- ./site.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "127.0.0.1:21600:80"
whoami:
image: traefik/whoami:v1.11.0
volumes:
cs-data:
cs-config:
El fichero appsec.yaml es la adquisición del componente AppSec. El comodín crowdsecurity/appsec-bot-* carga de una vez la configuración de puntuación, su umbral y las exclusiones que trae la colección:
name: appsec-bots
source: appsec
listen_addr: 0.0.0.0:7422
appsec_configs:
- crowdsecurity/appsec-default
- crowdsecurity/appsec-bot-*
labels:
type: appsec
site.conf sustituye al servidor por defecto de la imagen y manda todo al origen. El bouncer ya se engancha a nivel http desde el crowdsec_openresty.conf de la imagen, así que no hace falta tocar nada más:
server {
listen 80;
location / { proxy_pass http://whoami:80; }
}
Tras docker compose up -d, el registro de CrowdSec confirma el reto con WAF challenge runtime initialized y pow_difficulty=20. También avisa de que no hay master_secret: con una sola instancia vale, pero cada reinicio invalida las cookies ya emitidas.
Cómo comprobar que el bouncer sirve el reto
Una petición sin cookie devuelve un 200 con la página del reto en lugar de la respuesta del origen. Este comando muestra el estado y parte en líneas la CSP propia del reto:
curl -s -D - -o /dev/null http://127.0.0.1:21600/contacto/ \
| grep -i -E '^HTTP|^content-security' | tr ';' '\n'
La salida confirma que la respuesta viene del reto y no de whoami:
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'
script-src 'self' 'unsafe-inline'
style-src 'self' 'unsafe-inline'
img-src 'self' data:
worker-src 'self' blob:
La documentación habla de "a small HTML body", pero la página pesó entre 294 y 300 KB según la petición. En la primera, 245.397 de sus 296.641 bytes eran el módulo de JavaScript ofuscado en línea, y aparte se descargan fpscanner.js (36.315 bytes) y pow-worker.js (7.544 bytes). Con el anfitrión a una carga media de 83 sobre 18 núcleos, servirla costó entre 3,1 y 3,3 ms de mediana en tres tandas de 21 peticiones. Una ruta eximida tardó entre 1,2 y 2,2 ms.
Repetí la petición con rutas y clientes distintos. Estas son las respuestas:
| Petición | Resultado |
|---|---|
GET / y GET /blog/post-1 con curl |
página del reto |
GET / con User-Agent de Googlebot o de GPTBot |
página del reto |
POST /contacto con un formulario |
página del reto |
/robots.txt, /sitemap.xml y /feed/ |
origen |
/api/v1/items y /wp-json/wp/v2/posts |
origen |
/assets/app.css y /webhooks/stripe |
origen |
/.env |
403 del WAF, regla vpatch-env-access |
Las rutas que llegan al origen son las exclusiones por ruta de la colección. El POST merece atención: un formulario enviado sin cookie recibe la página del reto y su cuerpo nunca llega a la aplicación.
Qué ve CrowdSec cuando llega un navegador automatizado
Un Chromium manejado por Playwright resuelve la prueba de trabajo y aun así se queda fuera. Lancé Chromium 154.0.8037.0 con Playwright 1.64.0-alpha-2026-09-14 en modo sin interfaz: envió el reto a los 216 ms y recibió "Verification rejected". La alerta queda en cscli alerts list --kind bot-detection, y este filtro saca el motivo y el desglose:
cscli alerts inspect 3 -o json | jq -r '.meta[]
| select(.key=="fail_reason" or .key=="score_reasons")
| .value | fromjson[]' | tr ',' '\n'
Cinco señales de 100 puntos suman 500, casi siete veces el umbral por defecto:
request score 570
cdp=100
webdriver=100
webdriver_writable=100
webdriver_iframe=100
bot_user_agent=100
missing_chrome_object=50
utc_timezone=15
swiftshader_renderer=5
El mismo navegador con interfaz, bajo Xvfb, bajó a 420 puntos. Desaparecen bot_user_agent y missing_chrome_object, pero cdp y las tres marcas de webdriver siguen sumando 400, así que tampoco pasaría con el umbral permisivo de 100. Cualquier automatización por el protocolo de DevTools deja la marca cdp, y eso incluye a los agentes que navegan con browser-use. No probé navegadores sin interfaz escritos desde cero, como Obscura; su resultado dependerá de qué señales expongan.
Un envío rechazado deja una alerta de tipo bot-detection, pero no una decisión. Un solo intento no bloquea a nadie.
Cuánto tarda un visitante real en pasar el reto
Un Chromium sin automatizar pasó el reto en 1,6 s de mediana, y la mitad de ese tiempo es una pausa fija. Lo lancé tres veces sin DevTools, con perfil nuevo, zona horaria Europe/Madrid, ventana de 1600×900 en Xvfb y la API de resumen desactivada, por el motivo que explica la subsección siguiente. El registro de OpenResty da estos tiempos, contados desde la primera petición:
| Intento | Envío del reto | Página del origen |
|---|---|---|
| 1 | 0,447 s | 1,268 s |
| 2 | 0,797 s | 1,605 s |
| 3 | 0,828 s | 1,639 s |
La carga media del anfitrión rondaba 73 sobre 18 núcleos durante las tres pruebas, y los tiempos de envío la incluyen. Los 0,8 s entre el envío y la página real no dependen de la máquina: el script del reto espera 800 ms con setTimeout antes de recargar. La cookie resultante es __crowdsec_challenge, de 2.204 caracteres, con Max-Age=43199 (12 horas), HttpOnly y SameSite=Lax.
Un navegador que nunca termina el reto
El primer intento con ese Chromium no pasó: se quedó en "Consulting the crowd" durante más de 45 s sin enviar nada.

La causa está en fpscanner. Entre sus sondas consulta Summarizer.availability(), la API de resumen integrada en Chrome, y espera la respuesta sin límite de tiempo. Como todas las sondas se reúnen con Promise.all, una que no responde bloquea la huella entera. El código fuente de fpscanner[5] ya documenta el problema para otro navegador: "Opera exposes Summarizer but availability() never settles, hanging the entire collection."
Al arrancar el mismo binario con --disable-blink-features=AISummarizationAPI, envió el reto en menos de un segundo. Lo que probé es Chrome for Testing 154 en Linux arm64, no el Chrome estable de un usuario, y no sé si ahí la llamada responde. Sí sé que el síntoma existe y que la página no avisa: el visitante ve la animación sin fin. Si activas el reto, vigila en cscli metrics show bot-detection la diferencia entre retos pedidos y enviados.
Cómo evitar que tu CSP congele el reto
Si tu servidor web añade una Content-Security-Policy (CSP) sin 'unsafe-inline' o sin blob:, el reto no llega a ejecutarse. La página del reto trae su propia CSP permisiva, pero nginx añade la tuya al lado y el navegador aplica las dos. Lo reproduje con un segundo servidor que añade default-src 'self'; script-src 'self': Chromium registró 6 errores de CSP, la página quedó sin estilos y nunca envió el reto.

Hay un matiz que la documentación de CrowdSec no menciona. Si tu política usa nonces, añadir 'unsafe-inline' no te salva. La referencia de CSP de MDN[6] lo explica: si una directiva lleva un nonce, "the browser ignores unsafe-inline". Es el caso de este blog, cuyo nginx envía un script-src con un nonce distinto en cada petición.
El arreglo que propone CrowdSec es añadir tu política solo cuando la respuesta no trae ya una. Un map sobre la cabecera saliente deja la variable vacía para la página del reto, y nginx no envía una cabecera vacía:
map $sent_http_content_security_policy $hdr_csp {
"" "default-src 'self'; script-src 'self'; object-src 'none'";
}
server {
listen 82;
add_header Content-Security-Policy $hdr_csp;
location / { proxy_pass http://whoami:80; }
}
Con ese cambio en un tercer servidor, la página del reto llevó una sola CSP, la suya. Playwright llegó a enviar el reto, y fue rechazado como antes. La ruta /robots.txt, que sí llega al origen, siguió saliendo con la política del sitio. Si tu política ya sale de otro map, como la de este blog, encadena los dos en lugar de sustituir el tuyo.
Cómo limitar el reto a las rutas que importan
Por defecto el reto cae sobre cada página HTML, y en un blog eso obliga a pasar la prueba a cada lector que llega desde un buscador. La documentación de personalización[7] propone una configuración superpuesta que exime todo lo que no quieras proteger. Esta limita el reto al formulario de contacto y al acceso de WordPress:
name: local/reto-solo-formularios
inband:
pre_eval:
- filter: >-
!(req.URL.Path startsWith "/contacto"
|| req.URL.Path startsWith "/wp-login.php")
apply:
- ExemptFromChallenge("fuera-de-formularios")
La copié en /etc/crowdsec/appsec-configs/, añadí local/reto-solo-formularios a appsec_configs (el comodín crowdsecurity/appsec-bot-* no la incluye) y recargué CrowdSec. Después, / y /blog/entrada llegaron al origen, y /contacto/ y /wp-login.php recibieron el reto.
Qué pasa con Googlebot y los rastreadores de IA
Los rastreadores conocidos se libran del reto solo si CrowdSec verifica que la IP es suya. Las configuraciones appsec-bot-challenge-exclude-* llaman a MatchKnownBot(), que comprueba los rangos publicados por cada empresa o una resolución DNS inversa confirmada. Mi curl con el User-Agent de Googlebot desde una IP privada recibió el reto, que es lo correcto; el camino contrario no pude probarlo porque no tengo IP de Google.
La lista de bots es más larga de lo que cuenta la descripción de la configuración por defecto[8]. Su tabla nombra 24, pero las configuraciones que instaló el hub el 16 de septiembre de 2026 verifican 40:
| Configuración | Bots | Ejemplos |
|---|---|---|
exclude-search-engines |
17 | googlebot, bingbot, applebot, duckduckbot, ahrefs, semrush |
exclude-ai-crawlers |
12 | gptbot, openai-searchbot, perplexitybot, anthropic, mistralai-user |
exclude-social |
7 | meta, discord, telegram, twitterbot, linkedin |
exclude-monitoring |
4 | uptimerobot, cookiebot, datadog, pagerduty |
Para el posicionamiento, lo que importa es que un rastreador ausente de esa lista ve la página del reto y no tu contenido. Revisa tus registros antes de activarlo en todo el sitio: si falta un bot que te interesa, añádelo con un fichero propio de bots conocidos o exime su ruta.
Cuándo el reto acaba en un bloqueo de IP
Un reto rechazado solo deja una alerta; el bloqueo lo crean los dos escenarios de comportamiento que instala la colección. El escenario appsec-bot-challenge-too-many-requests es un cubo con fugas de capacidad 10 y fuga cada 20 s, que cuenta retos servidos y nunca enviados. Con 14 peticiones seguidas desde curl, el escenario saltó con 11 eventos en 48,7 ms y CrowdSec creó un bloqueo de 4 horas. El bouncer lo aplicó alrededor de un segundo después, y desde entonces la IP recibió un 403 incluso en /robots.txt.
Antes tuve que quitar el analizador crowdsecurity/whitelists. Todas mis peticiones venían de 172.21.0.1, la pasarela de Docker, y esa lista blanca descarta las IP privadas: 22 de 22 eventos quedaron fuera y el escenario no se disparaba. Si pruebas desde tu red local, te pasará lo mismo.
Límites de una función en alfa
Lo que funciona hoy puede cambiar en la próxima versión, y hay costes que conviene aceptar antes de activarla:
- Fija las versiones: CrowdSec 1.8.1, la colección
crowdsecurity/appsec-bot-challenge0.7 y el bouncer 1.2.3; actualiza a propósito, no por arrastre delatest. - Con más de una instancia de AppSec,
master_secretykey_rotation_intervaltienen que coincidir o las cookies de un nodo no valen en otro. - Cualquier API o sonda fuera de una ruta eximida recibe el reto; exímela o dale cookie con
GrantChallengeCookie. - La ofuscación solo retrasa al atacante, y la página de configuración[9] lo admite: "obfuscation buys time and cost, not invisibility".
- El contenedor de CrowdSec ocupaba 311 MiB de memoria con 198 reglas en banda y el reto activo.
Quedaron sin probar el plugin de Traefik, HAProxy, Envoy, un anfitrión amd64, el atributo Secure de la cookie bajo HTTPS y los navegadores Firefox, Safari y Chrome estable.
Preguntas frecuentes
¿La detección de bots de CrowdSec sustituye a un captcha?
Para separar navegadores de scripts, sí, y sin depender de un tercero. La remediación por captcha del bouncer necesita reCAPTCHA, hCaptcha o Turnstile; el reto de la 1.8 se calcula y se valida en tu propio componente AppSec. No pide nada al visitante, pero exige JavaScript y cookies.
¿Bloquea a Googlebot y perjudica el SEO?
No, si Googlebot llega desde sus propias IP: la configuración exclude-search-engines lo verifica y lo deja pasar sin cookie. Un rastreador que no esté en las listas sí ve el reto, así que decide qué bots te interesan antes de activarlo en todo el sitio.
¿Funciona con Traefik?
Solo con la versión preliminar 1.8.0-alpha del plugin de maxlerebourg, publicada el 12 de septiembre de 2026. No la he probado: en este artículo el reto corre detrás del bouncer de OpenResty 1.2.3.
Conclusión
La detección de bots de CrowdSec 1.8 cumple con los navegadores automatizados: Playwright no pasó ni con interfaz ni sin ella, y un Chromium limpio sí. Los fallos están alrededor: una CSP del sitio congela el reto, sin appsec-generic-rules CrowdSec ni arranca y un navegador cuya API de resumen no responde espera sin fin. Actívala primero en las rutas que atraen abuso, como formularios y accesos, fija versiones y vigila la diferencia entre retos pedidos y enviados. La versión en inglés de este artículo está en How to enable CrowdSec 1.8 bot detection.
Fuentes
- documentación de la detección de bots
- página de activación
- código del reto en la 1.8.1
- plugin 1.8.0-alpha
- código fuente de fpscanner
- referencia de CSP de MDN
- documentación de personalización
- descripción de la configuración por defecto
- página de configuración
- CrowdSec, notas de la versión 1.8.0
- CrowdSec, notas de la versión 1.8.1
- cs-openresty-bouncer, versión 1.2.3
Código fuente
Accede a todo el código fuente de este artículo en GitHub.
Ver en GitHub