Skip to main content
Security Engine version:
Version: Next

Enable bot detection

This page covers turning bot detection on: installing the collection, making sure your AppSec acquisition loads it, and verifying that the challenge is served. Check the prerequisites first.

warning

Check that your bouncer supports bot detection before going further, see the list of compatible bouncers. Enabling it behind a bouncer that does not support it leads to unexpected behavior, most likely silently refusing every client.

Install the collection

Install the collection that bundles everything (appsec-configs + scenarios + parsers):

SH
sudo cscli collections install crowdsecurity/appsec-bot-challenge

Then make sure the bundled appsec-configs are actually loaded by your AppSec acquisition. Open the AppSec datasource file (typically /etc/crowdsec/acquis.d/appsec.yaml):

YAML
listen_addr: 127.0.0.1:7422
appsec_configs:
- crowdsecurity/appsec-default
- crowdsecurity/appsec-bot-*
labels:
type: appsec

The wildcard matches installed appsec-configs only, so it picks up exactly what the collection gave you: the scoring engine, its threshold, and every exclusion config. A broader crowdsecurity/* works too.

warning

Install one bundle, not several. appsec-bot-challenge, -strict and -permissive each ship a different threshold config, and a wildcard loads every one you have installed. Their rejection rules then stack and the strictest threshold wins, silently overriding your choice. If you switch bundles, remove the old one rather than installing on top.

The exclusion configs cover verified search-engine, AI, social and monitoring bots plus well-known paths. See Known bots it lets through and Path-exclusion configs for what each one covers. If some don't apply to your app, list the configs you want by name instead of using the wildcard.

Reload CrowdSec for the change to take effect:

SH
sudo systemctl reload crowdsec

Choosing a threshold

The collection installed above rejects a submission scoring 75 or more. To be stricter or more forgiving, install a different bundle and point the acquisition at its threshold config:

CollectionThreshold config to loadRejects at
crowdsecurity/appsec-bot-challengecrowdsecurity/appsec-bot-challenge-scoring-balanced>= 75
crowdsecurity/appsec-bot-challenge-strictcrowdsecurity/appsec-bot-challenge-scoring-strict>= 45
crowdsecurity/appsec-bot-challenge-permissivecrowdsecurity/appsec-bot-challenge-scoring-permissive>= 100

Start with the default, then read score_reasons on real rejections before you move it. How a request is scored covers what each threshold buys you.

Once installed, see default configuration for a tour of the behavior you just enabled — none of it requires an extra install step.

Verification

Hit a protected route from a clean client (no cookie): you should receive the challenge HTML:

SH
curl -i https://your-protected-site.example/some/page
# expect a 200 with a small HTML body containing the challenge script,
# and a Set-Cookie for __crowdsec_challenge once the challenge is solved.

Tail the CrowdSec log and trigger a failed submission (e.g. with curl against /crowdsec-internal/challenge/submit with garbage payload) to see the on_challenge_submit rejected line. After enough failed submissions, the behavioral scenario should fire and appear in cscli alerts list.

Get rejected as a bot (CDP)

To see the bad-bot rejection path end to end, visit a protected route with a browser driven over the Chrome DevTools Protocol (CDP). The fingerprint library flags CDP as a high-severity automation signal, rejecting it. Any CDP-based automation triggers it: Puppeteer, Playwright with Chromium, or plain Chrome launched with --remote-debugging-port (or open debug-tools).

Example Puppeteer script
JS
// node cdp-check.js — drives Chromium over CDP, so the challenge should reject it
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://your-protected-site.example/some/page", { waitUntil: "networkidle0" });
console.log("status:", (await page.content()).length, "bytes");
await browser.close();
})();

The browser solves the proof-of-work and submits a valid challenge, but the fingerprint carries the cdp signal, worth 100 points on its own. Tail the log and you should see the rejection:

TEXT
level=info msg="on_challenge_submit rejected" automation=true is_bot=true platform=Linux reason="request score 115" signals="[cdp utc_timezone]" source="::1" timezone=UTC ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"

reason reports the score that crossed the threshold. Here the container also reported a UTC timezone, so utc_timezone added its 15 points on top of cdp.

A clean browser (no automation, no DevTools attached) visiting the same route gets the cookie and passes.

What the alert looks like

A rejection is not just a log line. It becomes an alert with kind bot-detection:

TEXT
$ sudo cscli alerts list --kind bot-detection --limit 1
+------+--------+-------------------------------------------+---------+----+-----------+----------------------+---------------+
| ID | value | reason | country | as | decisions | created_at | kind |
+------+--------+-------------------------------------------+---------+----+-----------+----------------------+---------------+
| 2019 | Ip:::1 | crowdsecurity/rejected-browser-submission | | | | 2026-08-18T11:41:11Z | bot-detection |
+------+--------+-------------------------------------------+---------+----+-----------+----------------------+---------------+

Inspect it to see why the client was refused:

TEXT
$ sudo cscli alerts inspect 2019 -d

- Context :
+------------------+--------------------------------------------------------------+
| Key | Value |
+------------------+--------------------------------------------------------------+
| bot_detected | true |
| challenge_event | rejected |
| fail_reason | request score 115 |
| fingerprint_id | FS1_000010000000000100000_00010h02ba_1280x1024c02m08b00011h4 |
| | 0bcfb_f10001111000101111000111111111e00000000p1100h-2d436_0h |
| | 6526db_1h-53968_en2tUTC_h-48c_0100h6ae994 |
| operating_system | Linux |
| request_score | 115 |
| score_reasons | cdp=100,utc_timezone=15 |
| target_uri | / |
| user_agent | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, |
| | like Gecko) Chrome/151.0.0.0 Safari/537.36 |
+------------------+--------------------------------------------------------------+

score_reasons is the field that explains the decision. See The alerts it produces for the full field reference and how these look in the Console.

If you don't see any challenge activity after a reload, double-check that:

  • The new appsec-config is listed in your AppSec datasource (appsec_configs:).
  • The bouncer is forwarding /crowdsec-internal/challenge/* paths unchanged.

Metrics

Bot detection exposes the challenge lifecycle as Prometheus counters and surfaces a summary in cscli. The funnel is requestedsubmittedaccepted (solved or granted) or rejected (protocol, submission, or cookie).

The dedicated bot-detection section shows the per-engine breakdown:

TEXT
$ sudo cscli metrics show bot-detection
+----------------------------------------------------------------------------------------------------------------------------------+
| Bot Detection Metrics |
+-----------------+-----------+-----------+--------+---------+--------+-------------------+----------------------+-----------------+
| Bot Detection | Requested | Submitted | Solved | Granted | Exempt | Protocol Failures | Submissions Rejected | Cookies Invalid |
+-----------------+-----------+-----------+--------+---------+--------+-------------------+----------------------+-----------------+
| 127.0.0.1:7422/ | 3 | 2 | 1 | - | 2 | - | 1 | - |
+-----------------+-----------+-----------+--------+---------+--------+-------------------+----------------------+-----------------+
+----------------------------------+
| Bot Detection — Exempted |
+-----------------+--------+-------+
| Appsec Engine | Reason | Count |
+-----------------+--------+-------+
| 127.0.0.1:7422/ | static | 2 |
+-----------------+--------+-------+
| Total | | 2 |
+-----------------+--------+-------+
+----------------------------------------------------------+
| Bot Detection — Rejected |
+-----------------+------------+-------------------+-------+
| Appsec Engine | Kind | Reason | Count |
+-----------------+------------+-------------------+-------+
| 127.0.0.1:7422/ | submission | request score 115 | 1 |
+-----------------+------------+-------------------+-------+
| Total | | | 1 |
+-----------------+------------+-------------------+-------+
+-----------------------------------------+
| Bot Detection Infrastructure Metrics |
+---------------------------------+-------+
| Metric | Count |
+---------------------------------+-------+
| Signing key regenerated | 1 |
| Signing key evicted | 0 |
| Re-obfuscation (dynamic module) | 1 |
| Dynamic module evicted | 0 |
+---------------------------------+-------+

Read the funnel across the first table: 3 challenges served, 2 submitted, 1 solved and 1 rejected. The Exempt column counts requests an exclusion config skipped, here 2 static assets. The Rejected table groups by the reason string, so with the shipped configs it doubles as a histogram of the scores you are turning away.

The top-level appsec table also gains a three-column challenge summary:

TEXT
$ sudo cscli metrics show appsec-engine
+-------------------------------------------------------------------------------------+
| Appsec Metrics |
+-----------------+-----------+---------+---------------+--------------+--------------+
| Appsec Engine | Processed | Blocked | Ch. Requested | Ch. Accepted | Ch. Rejected |
+-----------------+-----------+---------+---------------+--------------+--------------+
| 127.0.0.1:7422/ | 12 | 1 | 3 | 1 | 1 |
+-----------------+-----------+---------+---------------+--------------+--------------+

The full list of Prometheus metric names and labels lives in the Application Security Engine section of the Prometheus reference.

CrowdSec Docs
We use cookies

This site uses cookies to help us improve your experience. You can accept or decline below.