How it works
Bot detection interposes a browser-side proof-of-work + device-fingerprint check between a visitor and your application. A client that clears it receives a sealed cookie and is not challenged again until the cookie expires; a client that fails — or never tries — is filtered out.
Step by step:
- A request with no valid
__crowdsec_challengecookie reaches a protected route, triggeringSendChallenge. - Instead of the origin response, the AppSec component serves a challenge page that pulls in the JavaScript doing the work: the open-source fpscanner fingerprinting library (served as-is, public code), a small proof-of-work / crypto bundle, and a per-epoch signing-key module. Only the last two are obfuscated — fpscanner is not — see JS obfuscation.
- The browser collects a device fingerprint and solves a proof-of-work puzzle (its cost is tunable with
SetChallengeDifficulty— see Challenge difficulty levels), then POSTs the result to/crowdsec-internal/challenge/submit. - The AppSec component cryptographically validates the submission and decrypts the fingerprint. The client will be rejected if it identifies as automation, or let them through.
- On acceptance, the client receives a sealed success cookie carrying the fingerprint and an expiry timestamp. Subsequent requests present the cookie and will pass — without re-challenging.
- Signing keys rotate on a schedule (
key_rotation_interval), but the cookie is sealed under a separate long-lived key, so rotation does not invalidate already-issued cookies. A singlemaster_secretroots every key and must be shared across instances in an HA deployment — see Key management. - Legitimate non-browser clients (search crawlers, uptime probes, …) are recognised per request by the
appsec-bot-challenge-exclude-*configs — which match them withMatchKnownBot()(IP-range or forward-confirmed reverse-DNS verified) and flag them withExemptFromChallenge(reason)— and skipped, with no cookie minted. See Known bots it lets through. - Per-request hooks only see one request at a time. The behavioral scenarios shipped by the collection watch across requests (too many challenge requests, too many submissions, never-submits) and create decisions that block repeat offenders at the bouncer.
The challenge runtime is built lazily — it only spins up if a loaded hook references SendChallenge(), GrantChallengeCookie(), or RejectSubmission(). Installing the bot-detection collection is what turns it on.
Device fingerprinting is powered by fpscanner, an open-source library by Antoine Vastel.
What the challenge collects
When a browser runs the challenge, the open-source fpscanner library (by Antoine Vastel) gathers a broad device fingerprint and posts it back (encrypted) with the proof-of-work. The AppSec component decrypts it into a FingerprintData object and derives the bot-detection verdict from it. The data points fall into a few families:
- Automation — webdriver / Selenium / CDP / Playwright indicators. Feeds
HasAutomationSignal(). - Device — CPU count, memory, platform, screen geometry, media devices, and CSS media queries. Implausible values (e.g. impossible core counts or memory) feed
HasImpossibleDeviceSignal(). - Browser — User-Agent, a browser-feature bitmask, installed plugins and extensions, and UA client-hints. Headless tells here feed
HasHeadlessSignal(). - Graphics — WebGL and WebGPU vendor/renderer, plus canvas fingerprint.
- Codecs — audio/video capability hashes.
- Locale — timezone and languages.
- Cross-context checks — the same signals re-read inside an iframe and a web worker; disagreement across contexts feeds
HasMismatchSignal().
The high-level fingerprint.IsBot() verdict is true when any of these signal families fires; the individual helpers let you branch on a specific class (see Using the bot signal).
The exact set of collected data points is not fixed: because it tracks fpscanner, the fields and signals evolve as the library adds new detections and browsers change. Treat the list above as the current shape rather than a stable contract — for the exhaustive, always-current list of fields, see the exported Go type: FingerprintData.