The daemon is configured entirely through environment variables and two on-disk files. No code change is required to tune behaviour.

Environment variables

VariableTypeDefaultPurpose
ABUSIVE_FAMILYv4 / v6 / anyanyRestrict matching to one IP family.
ABUSIVE_MIN_HITSinteger ≥ 13Hits from one source before the IP is written to the blocklist.
ABUSIVE_SCOPEcomma-listrequest,uaWhich log fields to evaluate. Valid: request, ua, referrer.
ABUSIVE_REFERRER_DOMAINSwhitespace/comma list(empty)Allowed referrer hosts. Only used if referrer is in ABUSIVE_SCOPE.
ABUSIVE_SYSLOG_TAGstringabusive_http_watchSyslog program tag.
ABUSIVE_SYSLOG_FACILITYstringdaemonSyslog facility. Use local0local7 for custom routing.
ABUSIVE_SYSLOG_PRIOinfo / notice / warn / errnoticeSyslog priority for added events.
ABUSIVE_DEBUG0 / 10Verbose stderr trace of every line. Useful only during scoping.

Notes

  • ABUSIVE_MIN_HITS — Setting this to 1 blocks on the first hit. That is more aggressive but increases the false-positive risk from one-shot probes that happen to traverse a benign cloud IP later. The default of 3 is a good compromise.
  • ABUSIVE_SCOPEreferrer is off by default because a malicious referrer can put your visitor’s IP into the blocklist via a drive-by. Only enable referrer if you also set ABUSIVE_REFERRER_DOMAINS to a strict allowlist.
  • ABUSIVE_FAMILY — Useful when you want to mirror the daemon across two processes: one for IPv4 and one for IPv6, each writing to its own blocklist file.

Token set

The detection regex set is hardcoded in the script (@TOKENS) and covers eight attack classes — PHP exploitation, WordPress probes, path traversal, sensitive paths, RCE/CGI attempts, SQL injection, login-loop probes, and null-byte injection.

To tune the set:

  1. Open the script.
  2. Adjust the @TOKENS array — add patterns, remove ones that fire false positives in your environment.
  3. Restart the daemon.

The token set is deliberately conservative. Adding tokens like /admin, /login, or phpmyadmin without scoping will cause false positives on legitimate traffic.

Whitelist file

Path: /etc/abusive_http_whitelist

One entry per line. Three formats are supported:

# Comments allowed
203.0.113.42                    # single IPv4
2001:db8::abcd                  # single IPv6 (full or shortened)
203.0.113.0/24                  # IPv4 CIDR
2001:db8::/32                   # IPv6 CIDR

Whitelisted IPs are skipped completely — they never increment hit counters and never end up in the blocklist, even if their requests match every token in the set.

Use the whitelist for:

  • Office and VPN public IPs (operators triggering scanners by accident).
  • Search-engine crawler IP ranges that are documented to be legitimate (Google, Bing). Test before adding — the whitelist is rarely needed for crawlers because their requests do not match attack tokens.
  • Monitoring services that probe /wp-login.php deliberately.

Blocklist file

Path: /var/www/run/abusive_http_hosts

Format: one IP per line, no comments, no headers, no duplicates. The daemon enforces de-duplication via an in-memory hash and flock(LOCK_EX) on writes.

The file is append-only at runtime — old entries are never removed by the daemon. To prune (e.g. expire entries after 30 days), use external rotation:

# In a daily cron
mv /var/www/run/abusive_http_hosts /var/www/run/abusive_http_hosts.old
touch /var/www/run/abusive_http_hosts
chown _www:_www /var/www/run/abusive_http_hosts
doas rcctl restart abusive_http_watch

This forces the daemon to rebuild its in-memory state from an empty file. The firewall reloads the new (empty) blocklist on its next cron tick.

Firewall consumption

The blocklist file format is intentionally flat so that any layer can consume it. Common integrations:

OpenBSD pf

table <abusive> persist file "/var/www/run/abusive_http_hosts"
block in quick on egress from <abusive>

Reload table:

pfctl -t abusive -T replace -f /var/www/run/abusive_http_hosts

Linux nftables

table inet filter {
  set abusive_v4 {
    type ipv4_addr
    elements = { ... }
  }
  chain input {
    ip saddr @abusive_v4 drop
  }
}

Reload set from file via a small helper:

nft 'flush set inet filter abusive_v4'
awk '/^[0-9]/ {print}' /var/www/run/abusive_http_hosts | \
  xargs -I{} nft 'add element inet filter abusive_v4 { {} }'

Apache

<RequireAll>
  Require all granted
  Require not ip 203.0.113.0/24
  # … one Require not ip per blocklist entry
</RequireAll>

Generated by a templater (m4, sed) from the blocklist file on a cron. Slower to react than firewall integration, but works when you do not own the firewall layer.

nginx

geo $abusive {
  default 0;
  include /var/www/run/abusive_http_nginx.conf;
}
server {
  if ($abusive) { return 444; }
}

Where abusive_http_nginx.conf is generated by a template from the blocklist file.