Documentation
Staging
Per-rule observe and global shadow mode for safe rule rollout.
Staging policy changes
On this page
mod_botshield supports two complementary dry-run modes so you can deploy new rules without affecting production traffic until you're confident the matches are correct.
- Per-rule observe — pin a single directive into observe mode. Useful for staging individual rules.
- Scope-level
BotShieldEnabled LogOnly— flip every match to observe within the enclosing<VirtualHost>/<Location>/<Directory>regardless of per-rule setting. Useful for staging an entire policy revision, with per-<Location>carve-outs for paths you want to enforce immediately.
Either signal is sufficient; they compose with OR semantics. A rule
runs in observe mode if EITHER its per-rule mode is observe OR
the effective BotShieldEnabled for the request's scope is
LogOnly. There is no priority order to memorize.
What "observe" means precisely
When a rule fires in observe mode:
- The rule's predicate evaluates normally (path match, cohort match, env present, etc.).
- The decision log records the would-have-done outcome with an
:observesuffix on the reason token. - The matching observe-mode metric counter increments
(
block_path_observed_total,trigger_observed_total). - No flag bits are set on the IP.
- No score is added to the request.
- No status code, redirect, or log tag side-effect is emitted.
The audit trail captures everything the rule WOULD have done; the client response is unaffected. Observe matches never short-circuit the policy walk — the next rule still gets its chance.
Per-rule observe mode
Add mode=observe to any directive that supports it:
BotShieldPathTrigger /admin/.env flag=scanner_probe ttl=3600 log=admin-trap mode=observe
BotShieldBlockPath legacy-admin "/wp-admin/*" "" * mode=observe
BotShieldRateLimit api-burst 60 min "" * mode=observe
BotShieldFlagTrigger honeypot_hit action=tier_floor min=captcha mode=observe
The rule still evaluates against every matching request, but takes no action. Matches appear in the decision log:
mod_botshield: decision tier=pass outcome=allow ip=192.0.2.42
score=0 cookie=absent provider=- alg=- reason="path-trigger:admin-trap:observe"
path="/admin/.env"
Watch the log for hours or days. When matches look correct, remove
mode=observe and reload Apache to flip the rule into enforcement.
Scope-level BotShieldEnabled LogOnly
For staging a whole policy revision at once, set the directive at vhost scope:
BotShieldEnabled LogOnly
Every rule in scope flips to observe regardless of its per-rule
setting. Useful before a release or when comparing a new ruleset
against production traffic without any enforcement risk. Switch to
BotShieldEnabled On when you're ready to enforce.
BotShieldEnabled is tri-state: On / Off / LogOnly. It is
valid in RSRC_CONF | ACCESS_CONF scope, so per-<Location>
overrides give you fine-grained partial rollouts:
<VirtualHost *:443>
ServerName example.com
BotShieldEnabled LogOnly # most paths: observe
<Location "/login">
BotShieldEnabled On # /login: enforce now
</Location>
<Location "/healthcheck">
BotShieldEnabled Off # bare-metal probe: skip
</Location>
</VirtualHost>
The bs_dir_cfg merge picks the most-specific scope. An unset
inner scope inherits the outer.
Coverage
Both observe signals reach every gating surface:
| Family | Honors per-rule | Honors LogOnly |
Reason format |
|---|---|---|---|
| Path triggers | yes | yes | path-trigger:<name>:observe |
| Cookie triggers | yes | yes | cookie-trigger:<name>:observe |
| Env triggers | yes | yes | env-trigger:<name>:observe |
| Load triggers | yes | yes | load-trigger:<name>:observe |
| Feedback triggers | yes | yes | feedback-trigger:<event>:observe |
| Flag triggers | yes | yes | flag-trigger:<flag>:observe |
| Block-path | yes | yes | block-path:<name>:observe |
| Rate-limit | yes | yes | rate-limit:<name>:observe |
| Robots Disallow | n/a | yes | robots-block:<group>:observe |
| Form-captcha | n/a | yes | form-captcha:<scope>:observe |
Transport-level errors (415 / 413 / 400 on form-captcha; 503 on captcha-verify in-flight cap; 503 misconfigured) intentionally still fire under log-only mode — those are misconfiguration, not policy.
Tuning workflow
The full path from "draft policy" to "live enforcement":
-
Draft — write the new rule with
mode=observe(or setBotShieldEnabled LogOnlyto dry-run a whole revision). -
Reload —
apachectl configtest && systemctl reload apache2. -
Watch — tail the error log for
:observematches, or query the matching*_observed_totalPrometheus counter:curl -s http://localhost/botshield/metrics | grep observed # botshield_block_path_observed_total{name="legacy-admin"} 142 # botshield_trigger_observed_total{name="admin-trap",family="path"} 38 -
Iterate — if matches are wrong (false positives, missing cohort, broken predicate) edit and reload. The observe gate isolates iteration from production traffic.
-
Promote — remove
mode=observe(or changeBotShieldEnabled LogOnlytoOn) and reload. Watch the matching non-observe counter (tier_<t>_total,outcome_<o>_total) to confirm enforcement is happening.
Common pitfalls
Promoting individual rules from a log-only revision.
BotShieldEnabled LogOnly overrides every rule's per-rule mode. If
you switch the scope to On but the rule still has mode=observe,
the rule is still in observe — that's the OR semantics. Walk the
new rules and remove mode=observe before flipping the scope to
On.
Observe-mode logs not appearing. mod_botshield's decision log
emits at info. Apache's default LogLevel warn hides them.
Bump just this module:
LogLevel botshield_module:info
The *_observed_total Prometheus counters increment regardless of
log level — useful as a level-independent visibility check.
Counting observe matches in error log. The :observe suffix is
the canonical filter:
grep -c ':observe' /var/log/apache2/error.log
Counter aggregation across rule names is more efficient at scale — prefer Prometheus for production dashboards, log grep for forensics.
Where to next
- Tier model and scoring: site model.
- Per-family rule semantics: policy.
- Metrics + decision log surface: observability.