Skip to main content

Bot challenge

The bot challenge makes a visitor's browser solve a short proof-of-work puzzle before a gated path of a website loads. It runs in the server's edge, elpan-nginx, in front of every website on the server, so a request it stops never reaches Apache, PHP or a database. Account holders choose which paths of their websites it gates and in which mode; you choose, for the whole server, which crawlers the Adaptive mode lets through and which addresses are never challenged.

What a rule gates, the settings, what not to gate and what a visitor sees are the account holder's side: the account docs' Bot challenge.

How a request is gated​

Everything bound for Apache passes the gate after the rate limits. The edge looks the hostname up in the panel's rule tables; only for a gated host does it match the path, prefix by prefix, case-insensitively, on the decoded and normalized URI, so PATH_INFO, percent-encoding and dot segments don't get round a rule. A visitor without a valid pass gets the challenge page with status 403; the browser solves the puzzle, gets a cookie and reloads. The pass is bound to the website and to the client's address (the IPv4 address or the IPv6 /64), so one solved cookie can't be shared across a botnet. /.well-known/ is never gated.

It isn't bot detection: a headless browser solves it like a person does, and the gate makes high-volume abuse slow and costly. The rate limits and the ban daemon still do the volume work, and they run first: a flood on a gated path is shed with 429 before any challenge is served.

Always and Adaptive​

Each rule an account holder adds has a mode.

AlwaysAdaptive
Who solves the challengeEvery visitor, once per passOnly a client that sends requests faster than people do
Verified crawlersChallenged like anyone elseLet through, by the server-wide list and the website's additions
StopsEvery program without a browserScrapers, crawlers that ignore robots.txt, password guessing and floods from a few addresses
Doesn't stopA program that drives a real browserThe same, and a botnet sending a few requests from each of thousands of addresses

On an Adaptive path everyone gets in. The edge gives each browser a free elpan_seen cookie on its first HTML page and counts requests per cookie where there is one, else per address, so people behind one shared IPv4 address are told apart. A client over a threshold is flagged for the website for one pass lifetime, and while flagged its requests to the website's Adaptive paths go through the gate exactly as on an Always path: the challenge, a pass, its request budget and the difficulty ramp.

The thresholds are the edge's, not the website's. They sit below the rate limits, so an aggressive client gets a puzzle rather than a 429, and IPv4 addresses get double because they are often shared.

SignalPer cookiePer IPv6 /64 without a cookiePer IPv4 address without a cookie
Dynamic requests a minute6060120
Dynamic requests an hour6006001,200
POSTs a minute6612
Cookies issued an hour-530

Only dynamic requests count, not static files. Where a website's prefixes overlap, the longest decides the mode: /contact.php Always above / Adaptive challenges every form submitter and lets the rest of the website in unless a client is aggressive.

A new rule's mode follows the edge

While Adaptive is in its trial period a new rule starts as Always, so that it protects its path from day one; once the edge enforces Adaptive, a new rule starts as Adaptive. Rules made before Adaptive existed stay Always. Account holders can pick either when they add a rule.

The Adaptive trial period​

The first release of Adaptive (elpan-nginx 1.1.11) ships with enforcement off: Adaptive paths count, flag and log, and challenge nobody, while a week of real traffic calibrates the thresholds. The Bot Challenge screen says so while it lasts, and so does the account holder's tab.

Each new flag is one WOULD-FLAG line in the elpan-nginx journal (FLAG once enforcing), with the site, host, client, whether it was counted by cookie or by address, the signal (dyn-minute, dyn-hour, post-minute or cookies-hour), the count and the limit. To see what Adaptive would have done on your server:

journalctl -u elpan-nginx --since -7d -g 'WOULD-FLAG' | grep -o 'signal=[a-z-]*' | sort | uniq -c
journalctl -u elpan-nginx --since -7d -g 'WOULD-FLAG' | grep -o 'client=[^ ]* by=[a-z]*' | sort | uniq -c | sort -rn | head
Adaptive paths aren't protected during the trial

Turning enforcement on is a later elpan-nginx release, not a setting on the server or in the panel. Until then an Adaptive rule protects nothing. An account holder who needs protection now uses Always, which is also the panel's default for new rules meanwhile.

Who Adaptive lets through​

Bot Challenge in the System menu holds one setting: the crawlers that are never counted, flagged, challenged or given the cookie on any website's Adaptive paths. The default, with nothing chosen, lets the search engines' crawlers through and counts the AI companies' crawlers like anyone else. The screen says whether this server uses the default or its own choice, and Use the Default goes back to it.

  • Crawler kinds: Search engine crawlers (Googlebot, Bingbot, Applebot, DuckDuckBot and the other crawlers that index pages for a search engine) and AI crawlers (GPTBot, ClaudeBot, PerplexityBot and the other crawlers and fetchers of AI companies). A kind exempts every crawler of the kind.
  • Crawlers: single crawlers by name, in addition to the kinds, grouped the same way.

A crawler is recognized by the address ranges its operator publishes, never by the name it sends, which any scraper can copy. The ranges come from the elpan-nettags package, one list per crawler under /usr/share/elpan/nettags/crawlers/; the edge rebuilds its lookup table from them at every start and reload and whenever elpan-nettags is updated, so a fresh list is a package update (Updates). A missing or empty list exempts nobody, the safe direction; without elpan-nettags the screen says the kinds exempt nobody.

Account holders see your choice on their website's tab and can add to it: a kind, a crawler by name, or up to 50 addresses and networks of their own, such as an uptime monitor. A website only widens the exemption. On Always paths the crawler list is ignored: there every visitor pays.

Addresses never challenged​

The addresses exempt from the rate limits are exempt from the challenge too, in both modes, on every website: the loopback, the container bridge, the server's own addresses (so a WordPress cron request gets through), and any drop-in under /etc/elpan/nginx/ratelimit-exempt.d/. One file there, in geo syntax, exempts a client from the rate limits, the bans and the challenge at once, after a reload.

# /etc/elpan/nginx/ratelimit-exempt.d/10-office.lst
198.51.100.0/24 1;

This is the place for a monitoring system or a partner's server that must reach every website. An exemption for one website is the account holder's, on their tab.

The edge's files​

Two package-owned files in elpan-nginx hold the numbers the panel never writes. Both are root-only, loaded once per configuration load, and replaced on the next upgrade: a hand edit doesn't survive.

The difficulty file, /usr/share/elpan/nginx/pow/difficulty.json. The panel names a preset for each website (Very low to Very high); the file says how many bits of work each is (today 18, 20, 22, 24 and 26) and how the ramp works: a client that uses up a pass's requests, or more passes than its free allowance (10 for an IPv4 address, 3 for an IPv6 /64, in one pass lifetime), gets its next challenges one step harder, at most two steps and never above 26 bits. Hardware gets faster, so these numbers are revised by an elpan-nginx update and nothing else. Raising a preset re-challenges every holder of a pass on those websites; lowering it doesn't. The benchmark page at /.well-known/elpan/excuses/pow-benchmark/ on any website measures what each preset costs the device you open it on.

The Adaptive file, /usr/share/elpan/nginx/pow/adaptive.json: the enforcement switch, the flag lifetime and the thresholds above. The edge serves a copy at /.well-known/elpan/pow/adaptive.json on every host, and the panel reads the switch from it to word its screens.

The key, /var/lib/elpan/nginx/pow-key.conf, signs every challenge, pass and elpan_seen cookie. It's generated when missing and kept across restarts, so passes survive them. elpan-nginx-pow-rotate.timer rotates it weekly; the previous key is still accepted, so a pass signed before the rotation stays valid until it expires, at most 24 hours later. The counters (request budgets, the ramp, redeemed puzzles, Adaptive's counts and flags) live in the edge's shared memory: they survive a reload and are lost on a restart.

In the logs​

A challenge is a 403 in the website's access log, with the visitor's real method. The ban daemon counts 401 and 403 answers, sixty a minute from one client, as a sign of an attack; it's report-only by default, so look for WOULD-BAN on gated hosts before turning enforcement on. People in one office who open a gated page in the same minute all come from one address.

What you can't do from the admin app​

The rules, modes, settings and per-website exemptions of a website are on the Bot challenge tab of its website service in the account panel. The admin app has no copy of that tab. To change a website's rules, sign in as its account (Act in an account); the account's own API and MCP tools manage the same rules with the same checks.