Documentation
Deployment
Reverse proxies, slow-client defense, multi-vhost reputation, secret rotation, capacity sizing.
Deployment
On this page
- Behind a reverse proxy or load balancer
- Slow-client / slowloris defense
- HTTPS / `__Host-` cookie prefix
- Multi-vhost reputation
- Default: auto-isolation per ServerName
- Opt-in shared reputation: `BotShieldShareScope`
- Missing `ServerName`
- One SHM segment, scaling
- Capacity sizing
- Performance characteristics
- State persistence
- Secret rotation
- Where to next
This page covers real-world deployment topology: reverse proxies and load balancers, slowloris defense, multi-vhost reputation isolation, secret rotation, and capacity sizing.
Behind a reverse proxy or load balancer
mod_botshield keys every IP-based signal (flagged-IP table, Bloom
filter, rate limiters, score) on r->useragent_ip — the TCP peer
Apache sees. If Apache sits behind a reverse proxy or load balancer
(CDN, AWS ELB, nginx in front, etc.) that peer is the proxy, not the
real client. Without mod_remoteip, the module will flag your own
edge and challenge every legitimate visitor.
The fix is the stock Apache module mod_remoteip. Configure it to
trust your edge hops; it will rewrite r->useragent_ip to the real
client before any botshield hook runs:
LoadModule remoteip_module modules/mod_remoteip.so
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.0.0/8
RemoteIPTrustedProxy 2001:db8:cafe::/48
# ... one RemoteIPTrustedProxy per edge CIDR ...
This is the same module used for accurate %a in access logs — if
your access logs already show real client IPs, you're done.
We deliberately do not reimplement this. mod_remoteip parses
the configured header (typically X-Forwarded-For) as a comma-
separated chain of IPs, matches each hop against the trusted-
proxy CIDR list, and rewrites r->useragent_ip to the leftmost
untrusted address — IPv4 and IPv6, multi-hop chains, all in
Apache core, battle-tested, maintained. If your edge sends
RFC 7239 Forwarded: headers instead, terminate them at the
edge and have it forward X-Forwarded-For for Apache; mod_remoteip
itself does not parse the RFC 7239 grammar.
For a single-host deployment with no proxy in front, no extra
configuration is needed: r->useragent_ip already equals the client
IP.
Slow-client / slowloris defense
mod_botshield's body-read paths (form-captcha verify, the
captcha-verify endpoint, embedded-verify endpoint) inherit Apache's
slow-client defense. Apache's Timeout directive bounds how long a
worker can be held by a stalled client; the default is 60 seconds.
For production deployments, pair mod_botshield with
mod_reqtimeout. It gives finer-grained controls than Timeout and
is the standard Apache answer to slowloris-class attacks:
LoadModule reqtimeout_module modules/mod_reqtimeout.so
RequestReadTimeout header=20-40,minrate=500
RequestReadTimeout body=20,minrate=500
We deliberately do not implement our own slow-client defense.
mod_reqtimeout is in Apache core, applies to the whole vhost, and
mod_botshield's endpoints get the same protection as the
application's own handlers.
HTTPS / __Host- cookie prefix
When the request is HTTPS and you have not configured
BotShieldCookieDomain, mod_botshield emits the verified cookie as
__Host-bs_session. Browsers refuse to accept __Host- cookies
over plain HTTP, refuse cookies with a Domain= attribute, and
constrain Path=/ — closing several cookie-injection attack classes
that the legacy _bs_session name still has to support for HTTP
deployments.
Verify path checks both names (host-prefix first), so existing clients with the legacy cookie still work during the transition.
Multi-vhost reputation
Each vhost gets its own isolated bot reputation by default. A bot
flagged on site-a.example.com doesn't carry that flag to
site-b.example.com. Sites running many vhosts on one Apache
instance get per-site detection without configuring anything.
Default: auto-isolation per ServerName
Each vhost's reputation namespace is derived from its ServerName
directive — two vhosts with different ServerName values
automatically maintain separate reputation. No configuration required.
<VirtualHost *:443>
ServerName site-a.example.com
BotShieldEnabled on
</VirtualHost>
<VirtualHost *:443>
ServerName site-b.example.com
BotShieldEnabled on
</VirtualHost>
Different sites usually have different threat models — the strict isolation is what most sites want.
Opt-in shared reputation: BotShieldShareScope
When sibling vhosts should share state — dev/prod environments,
www/api subdomains under the same brand, redundant frontends — set
BotShieldShareScope to the same string on each vhost:
<VirtualHost *:443>
ServerName www.example.com
BotShieldShareScope example-cluster
</VirtualHost>
<VirtualHost *:443>
ServerName api.example.com
BotShieldShareScope example-cluster
</VirtualHost>
Both vhosts now share one reputation namespace. A bot flagged on either site is flagged on the other. The scope token is hashed; any string works, just keep it consistent across the vhosts you want grouped. Different tokens produce independent groups.
Missing ServerName
A vhost without a ServerName directive falls back to the global
default namespace (ns_id=0). All such vhosts share reputation;
mod_botshield logs a NOTICE at startup so the fallback is visible.
For explicit isolation on a vhost without ServerName, set
BotShieldShareScope to a unique-per-vhost token.
One SHM segment, scaling
A single Apache instance can handle hundreds of vhosts sharing one
shared-memory segment. The per-slot namespace tag lets all vhosts
coexist without per-vhost SHM segments. Monitor SHM utilization via
the headroom watchdog (logs at info) and tune capacity directives
for aggregate traffic.
Capacity sizing
Default budgets fit a single-site deployment seeing ~1M unique IPs per week. Larger deployments (or aggregations across many vhosts) should size capacity directives to match.
| Directive | Default | What it limits |
|---|---|---|
BotShieldShmSize |
16M |
Total SHM budget (header + tables + Bloom buffers). Range 128K..256M |
BotShieldFlaggedIPCapacity |
50000 |
Open-addressed slot count for flagged IPs. Range 1024..1000000 |
BotShieldBloomIPs |
1000000 |
Expected unique-IPs working set. Drives Bloom filter dimensions |
BotShieldBloomWindow |
604800 |
Bloom rotation window (seconds). Rotation at window/2 |
BotShieldRateLimitEscalateCapacity |
50000 |
strike-table slots |
BotShieldSafeguardCapacity |
50000 |
challenge-loop suppression slots |
BotShieldEmbeddedNonceCapacity |
32768 |
embedded-bootstrap nonce table |
These are server-scope only — placed inside <VirtualHost> they
emit a NOTICE and are ignored. The SHM segment is module-global, so
sizing happens once at the main-server level.
The headroom watchdog logs notices when any table approaches capacity, so you can size reactively rather than guess up-front. Look for log lines of the form:
mod_botshield: capacity headroom: flagged_ip 38241/50000 (76%)
Performance characteristics
The in-tree benchmark suite in tests/bench/ measures pass-through
cost against a tiny Apache static-file endpoint. That workload is
useful because it makes fixed per-request cost visible, but it is
also the harshest possible denominator: a 140-byte static response
on localhost is much cheaper than a normal application request.
Latest saved measurements combine the single-connection and
saturation wrk sweep (tests/bench/run-bench.sh) with the
fixed-rate oha sweep (tests/bench/run-rate-bench.sh). All run
against localhost with browser-like request headers:
| Measurement | Result | How to read it |
|---|---|---|
| Single connection, Apache static baseline | ~310us p50 | Cheapest local request path. |
Single connection, BotShieldEnabled on, no policy features |
~318us p50 | About +8us for the basic pass-through path. |
| Fixed-rate 1k / 5k rps | p50 and p99 deltas mostly noise | Non-saturating traffic stays flat. |
| Fixed-rate 10k rps, trigger-heavy policy | ~+0.03ms p50, ~+0.28ms p99 | Real cost, still well below normal app latency. |
| Fixed-rate 10k rps, kitchen-sink policy | ~+0.05ms p50, ~+2.7ms p99 | Broadest config shows tail growth before median pain. |
Saturation -t4 -c100, trigger-heavy / kitchen-sink policies |
~20% lower static-file RPS | Capacity-ceiling result, amplified by the tiny static baseline. |
The practical reading is: BotShield has a real per-request cost, but the absolute cost on passing traffic is small. The worst-looking RPS percentage comes from saturating Apache with a tiny static file over loopback; it should not be read as "every production request gets 20% slower." The fixed-rate pass is the better proxy for normal operation: even at 10k rps, median latency barely moves, and the largest effect is a tail-latency signal in the broadest policy configuration. For dynamic application traffic, network RTT, FastCGI/PHP startup, framework work, database calls, and template rendering usually dominate the few microseconds to low milliseconds BotShield adds.
Where the overhead earns its keep: every bot request handled before the backend avoids invoking the application at all. Avoiding even one framework bootstrap, database query, or upstream proxy trip can save more work than many pass-through BotShield checks cost.
The tests/bench/run-bench.sh and tests/bench/run-rate-bench.sh
drivers reproduce these measurements on your own hardware. Raw
wrk / oha output is saved under tests/bench/results/ so
you can inspect the full latency and throughput distribution
when sizing a deployment.
State persistence
mod_botshield can snapshot the SHM tables to disk at a configured interval (or only at clean shutdown) and reload on next start. This lets the flagged-IP table, Bloom filters, and rate-limit counters survive Apache restarts.
BotShieldStateFile /var/lib/botshield/state.bin
BotShieldStateSaveInterval 300
Both directives are server-scope. The default is "no persistence"
(BotShieldStateFile unset). Periodic saves require mod_watchdog;
the graceful-shutdown save runs regardless.
The state file format is versioned. Format-version drift between saves and loads (e.g. after upgrading the module) results in the table starting fresh with a NOTICE — never a startup failure.
Secret rotation
mod_botshield supports a verify-only secondary key for graceful HMAC rotation. The flow:
-
Generate a new master secret. Install it as a fresh file (e.g.
/etc/botshield/secret.new). -
Configure both files, with the new key as primary and the old key as secondary:
BotShieldSecretFile /etc/botshield/secret.new BotShieldSecondarySecretFile /etc/botshield/secret.oldReload Apache. New cookies are signed with the new key; existing cookies still verify against the secondary.
-
After the cookie TTL elapses (default 1 h, or
BotShieldCookieTTLif you raised it), every in-flight cookie has been re-issued under the new key. Drop the secondary:BotShieldSecretFile /etc/botshield/secret.newReload. The old secret can now be deleted.
The secondary covers four verify call sites: HMAC _bs_session,
GCM _bs_session, the captcha-pending cookie, and the
embedded-verify PoW path. App-bridge keys
(BotShieldAppIntegrationSecretFile) and captcha provider secrets
are out of rotation scope; rotate those by reloading with the new
file.
Where to next
- Tier model, scoring, decision log: site model.
- Allow lists, rate limits, triggers: policy.
- Captcha and app-bridge integration: captcha.
- Staging policy changes safely: staging.
- Metrics + decision log + mod_status: observability.