Repository navigation
06 Fail2Ban And AutoBan
Two optional layers turn repeated abuse into a ban. Both are disabled by default, both fail open,
and neither changes a verdict on its own (README.md, Fail2Ban and auto-ban):
| Layer | What it does | Where the state lives |
|---|---|---|
| Fail2Ban bridge | appends one PHPFSC BAN … line per hard event; a Fail2Ban filter and jail turn the lines into a firewall ban |
a log file the jail follows |
| Application auto-ban | counts hard events per IP and rejects the IP in the pipeline before any other check | the library's own storage (storage.*) |
The bridge ships in 2.1 as FormSpamChecker\Security\Fail2Ban; the auto-ban is the autoban
pipeline check (CHANGELOG.md, [2.1.0] Added).
One sanitized line per hard event, with an ISO-8601 timestamp first
(docs/WAF.md, Fail2Ban; CHANGELOG.md, [2.1.0] Security):
2026-11-03T22:35:00+03:00 PHPFSC BAN ip=203.0.113.5 site=example.ru form=register score=100 reason=spam:keywords
Fields: ip, site, form, score, reason. Lines are LF-only, field values are token-sanitized
so no whitespace or newline can be injected, and the file is appended with LOCK_EX. A reporting
error never changes a verdict. The leading ISO-8601 timestamp exists so the jail needs no
"match without timestamp" fallback — datepattern = ^%Y-%m-%dT%H:%M:%S%z, as recorded in
CHANGELOG.md, [2.1.0] Security.
Which events are reported is a configuration choice: every hard-spam verdict from the form
pipeline, every hard WAF block, or both (emit_on), filtered by a minimum score (min_score).
'fail2ban' => [
'enabled' => true,
'log_path' => __DIR__ . '/../var/log/sites/' . $site . '.f2b.log',
'site' => $site,
'emit_on' => 'both', // spam | waf | both
'min_score' => 60,
],Quote: README.md, Fail2Ban and auto-ban. The fail2ban.* keys and defaults are tabulated in
docs/CONFIG.md, section Fail2Ban and auto-ban (2.1):
| Key | Default | Meaning |
|---|---|---|
fail2ban.enabled |
false |
write PHPFSC BAN … lines |
fail2ban.log_path |
storage/fail2ban.log |
file the jail follows; site configs use var/log/sites/<site>.f2b.log
|
fail2ban.site |
'' |
token written into site=
|
fail2ban.emit_on |
both |
report form spam, WAF blocks, or both |
fail2ban.min_score |
60 |
minimum verdict or block score to report |
Fail2Ban filter /etc/fail2ban/filter.d/php-form-spam-checker.conf, reproduced verbatim from
docs/WAF.md, section Fail2Ban:
[Definition]
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S%%z
failregex = PHPFSC BAN ip=<HOST> .*$
ignoreregex =The jail in /etc/fail2ban/jail.d/php-form-spam-checker.local bans only HTTP and HTTPS so SSH is
never affected; keep administrative hosts in ignoreip (docs/WAF.md, Fail2Ban):
[php-form-spam-checker]
enabled = true
filter = php-form-spam-checker
logpath = /var/www/lib/PHP-Form-Spam-Checker/var/log/sites/*.f2b.log
maxretry = 1
findtime = 3600
bantime = 86400
action = iptables-multiport[name=PHPFSC, port="http,https"]
ignoreip = 127.0.0.1/8 ::1 <admin-ip>maxretry = 1 matches the fact that a single hard event is already a deliberate decision by the
pipeline or the WAF. Verify the filter against a sample log before reloading Fail2Ban:
fail2ban-regex /path/to/sample.log php-form-spam-checkerCommand and the advice to verify before reloading: docs/WAF.md, Fail2Ban.
The autoban check is the fallback for hosts without Fail2Ban: after N hard events inside a window
the IP is stored as banned for a TTL, and the check rejects it before any other check runs
(README.md, Fail2Ban and auto-ban).
'autoban' => ['enabled' => true, 'threshold' => 5, 'window' => 3600, 'ttl' => 86400],| Key | Default | Meaning |
|---|---|---|
autoban.enabled |
false |
enable the application-level ban |
autoban.threshold |
5 |
hard events inside the window before the ban |
autoban.window |
3600 |
counting window, seconds |
autoban.ttl |
86400 |
ban duration, seconds |
Defaults and meanings: docs/CONFIG.md, section Fail2Ban and auto-ban (2.1). Banned IPs live in the
same storage as rate-limit counters, so a ban:<ip> entry expires with autoban.ttl; AutoBan::unban()
removes an entry, and a failed storage call never blocks a form (docs/CONFIG.md).
Because storage is shared, an autoban is visible to every process that reads the same storage.path
— that is what makes it work across PHP-FPM workers (docs/CONFIG.md, Storage and cache paths).
The 2.1 deployment guidance recommends 'threshold' => 100 for AbuseIPDB: only IPs whose
confidence score is exactly 100 are treated as hard spam. The library default stays 90, and a
per-form override can raise it for a single form (README.md, AbuseIPDB setup; CHANGELOG.md,
[2.1.0] Added, Deployment guidance).
'abuseipdb' => [
'enabled' => true,
'api_key' => getenv('ABUSEIPDB_KEY') ?: '',
'threshold' => 100, // reference deployments: only confidence 100 becomes hard spam
'cache_ttl' => 21600, // 6 h per-IP cache: a burst costs one query
'fail_open' => true,
'circuit_breaker' => ['failures' => 3, 'cooldown' => 300],
],The comment values come from README.md, AbuseIPDB setup and docs/CONFIG.md, Full default tree.
The trade-off is explicit: a higher threshold means fewer blocked submissions and fewer false
positives on shared NAT addresses; a lower one blocks more aggressively. A ban derived from a
Reputation verdict therefore inherits that decision — which is why fail2ban.min_score (60 by
default) and emit_on are worth reviewing before the jail is enabled.
-
integrations/guard.phpruns the WAF layer whenwaf.enabledis set, then the form pipeline (docs/INTEGRATION.md, Fail2Ban and auto-ban). - A hard event from either layer is written to
fail2ban.log_pathwhen the bridge is enabled, andautobancounts it when the auto-ban is enabled. - The next request from that IP is rejected by the
autobancheck with a hard SPAM verdict — before any other check — or by the firewall ban created by the jail.
Keep log_path writable by the PHP user. Site configurations default it to
var/log/sites/<site>.f2b.log; the module default is storage/fail2ban.log (docs/CONFIG.md,
Fail2Ban and auto-ban (2.1)).
-
Integration — where
guard.phpsits and how a site is configured. - WAF — what counts as a hard WAF block.
repository · releases · MIT