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.
| Always | Adaptive | |
|---|---|---|
| Who solves the challenge | Every visitor, once per pass | Only a client that sends requests faster than people do |
| Verified crawlers | Challenged like anyone else | Let through, by the server-wide list and the website's additions |
| Stops | Every program without a browser | Scrapers, crawlers that ignore robots.txt, password guessing and floods from a few addresses |
| Doesn't stop | A program that drives a real browser | The 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.
| Signal | Per cookie | Per IPv6 /64 without a cookie | Per IPv4 address without a cookie |
|---|---|---|---|
| Dynamic requests a minute | 60 | 60 | 120 |
| Dynamic requests an hour | 600 | 600 | 1,200 |
| POSTs a minute | 6 | 6 | 12 |
| Cookies issued an hour | - | 5 | 30 |
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.
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
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.