Trap paths, and the error burst banning a client refused too often (closes #115)
check / check (push) Waiting to run

SWWAF_TRAP_PATHS: a request whose path, as a path rule sees it, is one
of them is a clear sign of attack, banned as a ban rule's match is; the
ban's notes give its trap_path. Checked after the rate limits, before the
rule files.

SWWAF_ERROR_BURST_THRESHOLD (default 30, or off): more refusals in a
minute after a block or ban rule or a trap path, or for a missing or
wrong token, ban the client as a broken limit does. Counted in
clients.json's minute_refusals; limit_hit error_burst, notes kind
refusals.

A token refusal is now the offence token_refused, and
smallwebwaf_offences_total counts every kind the history does.

Judgement call: the threshold is not lowered by a client's limit percentage.

Model: opus-5-5
This commit was merged in pull request #118.
This commit is contained in:
2026-10-08 06:44:55 +02:00
parent 5f3fb48809
commit 54779f08de
21 changed files with 1208 additions and 318 deletions
+250 -187
View File
@@ -31,29 +31,32 @@ you name by URL, which it fetches and keeps, with a file of AS numbers'
percentages fetched the same way, the DNS blocklists (DNSBL zones), which it
asks about each client in the background, AbuseIPDB, which it asks in the
background about each client that has committed an offence, and the decision
list of a CrowdSec engine you run, which it fetches and keeps as a blocklist.
`smallwebwaf` passes each request to the app and the app's answer back,
unchanged, within its timeouts and size limits, works out each client's address,
looks up its AS number and country unless you switch that off, bans a client
that sends too many requests or too many bytes, not counting those for the paths
you choose, with lower limits for the clients of the AS numbers and countries
you list, refuses a client that comes from a country you refuse or from a
network you refuse, refuses, limits or only notes a client a blocklist or a
DNSBL zone you name lists, or AbuseIPDB scores at or over the score you set,
list of a CrowdSec engine you run, which it fetches and keeps as a blocklist. So
are two parts of the last stage, which need no Core Rule Set: the trap paths and
the error burst. `smallwebwaf` passes each request to the app and the app's
answer back, unchanged, within its timeouts and size limits, works out each
client's address, looks up its AS number and country unless you switch that off,
bans a client that sends too many requests or too many bytes, not counting those
for the paths you choose, with lower limits for the clients of the AS numbers
and countries you list, refuses a client that comes from a country you refuse or
from a network you refuse, refuses, limits or only notes a client a blocklist or
a DNSBL zone you name lists, or AbuseIPDB scores at or over the score you set,
bans a client your CrowdSec engine's decision list lists until that decision
ends, lets the networks you choose through, checks each request against the rule
files and bans a client whose request is a clear sign of attack, keeps its bans,
each client's counters and history, GeoJS's answers, the last good copy of each
list it fetches, the DNSBL zones' verdicts, AbuseIPDB's scores and the AbuseIPDB
checks spent today in JSON files across restarts, takes in your edits of those
files, such as a ban you make, keep or lift, and of the rule files while it
runs, writes a JSON log line for every request, sends its log lines to a syslog
server too if you name one, sends an alert to a webhook, to Slack and to ntfy,
each if you name one, for each ban it makes or makes permanent, for traffic over
an anomaly threshold you set, for a client a blocklist, the CrowdSec decision
list, a DNSBL zone or AbuseIPDB lists, for GeoJS failing, a list it cannot
fetch, a DNSBL zone or AbuseIPDB that fails or refuses a query and the day's
AbuseIPDB checks used up, for a rule file or state file with an error and for a
files and the trap paths you name and bans a client whose request is a clear
sign of attack, bans a client it refuses again and again within a minute for a
rule, a trap path or a missing or wrong token, keeps its bans, each client's
counters and history, GeoJS's answers, the last good copy of each list it
fetches, the DNSBL zones' verdicts, AbuseIPDB's scores and the AbuseIPDB checks
spent today in JSON files across restarts, takes in your edits of those files,
such as a ban you make, keep or lift, and of the rule files while it runs,
writes a JSON log line for every request, sends its log lines to a syslog server
too if you name one, sends an alert to a webhook, to Slack and to ntfy, each if
you name one, for each ban it makes or makes permanent, for traffic over an
anomaly threshold you set, for a client a blocklist, the CrowdSec decision list,
a DNSBL zone or AbuseIPDB lists, for GeoJS failing, a list it cannot fetch, a
DNSBL zone or AbuseIPDB that fails or refuses a query and the day's AbuseIPDB
checks used up, for a rule file or state file with an error and for a
replacement of the lookup database it cannot read, serves Prometheus metrics to
a scraper that holds the metrics token, lets an admin who holds the admin token
list, add and lift bans and ask what it knows of a client, and in `observe` mode
@@ -152,37 +155,47 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
The log line of each request the rate limits count gives its client's
percentages below 100 and the settings that gave them, and so do the notes of
a ban for a lowered limit, and its alert.
- Bans a client that breaks a rate limit or a byte limit, as "Bans" in
[`SPEC.md`](SPEC.md) describes: the first ban lasts an hour, and a limit
broken again within a day of a ban ending bans for three times as long as that
ban, so 1, 3, 9, 27 and 81 hours; a ban that would last longer than seven days
is permanent instead. A ban covers the client's netblock: its IPv4 address, or
the netblock around it that `SWWAF_BAN_SCOPE_V4_PREFIX` sets, or its IPv6
group. While it lasts, every request from the netblock is refused with
`SWWAF_BAN_RESPONSE` after the static lists and before the country lists, so
the client is not looked up, and is not counted for the rate limits. A ban
- Bans a client that breaks a rate limit, a byte limit or the error burst, as
"Bans" in [`SPEC.md`](SPEC.md) describes: the first ban lasts an hour, and a
limit broken again within a day of a ban ending bans for three times as long
as that ban, so 1, 3, 9, 27 and 81 hours; a ban that would last longer than
seven days is permanent instead. A ban covers the client's netblock: its IPv4
address, or the netblock around it that `SWWAF_BAN_SCOPE_V4_PREFIX` sets, or
its IPv6 group. While it lasts, every request from the netblock is refused
with `SWWAF_BAN_RESPONSE` after the static lists and before the country lists,
so the client is not looked up, and is not counted for the rate limits. A ban
sets the client's counters back to zero. Each ban carries notes for deciding
whether to lift it: the limit, whether it is on requests or bytes, its window
and the requests or bytes counted in it, the client's percentage of that kind
of limit and the setting that gave it when a biased threshold lowered the
limit, the request that broke it, the client's AS number, AS name and country
once they are looked up, the blocklists, the CrowdSec decision list, DNSBL
zones and AbuseIPDB, with its score, that listed the client when the ban was
made, the netblock's requests since it was first seen, how many of them the
ban has refused, and how many bans the netblock had before, for a broken
limit, for a clear sign of attack, by an admin and for CrowdSec's decision. At
most `SWWAF_MAX_BANS` bans `smallwebwaf` made are kept, past, active and
permanent; past that, the earliest such ban of the netblock that has gone
longest without a request is dropped first. The bans whose cause is `admin`,
those you make or keep, are kept besides, and never dropped. `bans.json` shows
the bans and their notes, a restart lifts none, and you make, keep or lift a
ban by editing it (see "State files" below).
whether to lift it: the limit, whether it is on requests, bytes or refusals,
its window and the requests, bytes or refusals counted in it, the client's
percentage of that kind of limit and the setting that gave it when a biased
threshold lowered the limit, the request that broke it, the client's AS
number, AS name and country once they are looked up, the blocklists, the
CrowdSec decision list, DNSBL zones and AbuseIPDB, with its score, that listed
the client when the ban was made, the netblock's requests since it was first
seen, how many of them the ban has refused, and how many bans the netblock had
before, for a broken limit, for a clear sign of attack, by an admin and for
CrowdSec's decision. At most `SWWAF_MAX_BANS` bans `smallwebwaf` made are
kept, past, active and permanent; past that, the earliest such ban of the
netblock that has gone longest without a request is dropped first. The bans
whose cause is `admin`, those you make or keep, are kept besides, and never
dropped. `bans.json` shows the bans and their notes, a restart lifts none, and
you make, keep or lift a ban by editing it (see "State files" below).
- Checks each request against the rules of the rule files (see "Rule files"
below) after the rate limits, and before its body is read. A `log` rule that
matches is noted in the log line; a `block` rule refuses the request with
`403`, and bans no one; a `ban` rule refuses it with `SWWAF_BAN_RESPONSE` and
bans the client's netblock for a clear sign of attack. Matching stops at the
first rule that refuses. A client in `SWWAF_ALLOW_NETS` is not checked.
`403`, and bans no one for it, though the refusal counts toward the error
burst; a `ban` rule refuses it with `SWWAF_BAN_RESPONSE` and bans the client's
netblock for a clear sign of attack. Matching stops at the first rule that
refuses. A client in `SWWAF_ALLOW_NETS` is not checked.
- Checks each request against the trap paths `SWWAF_TRAP_PATHS` names, after the
rate limits and before the rule files, which it does not need. A request for
one is refused with `SWWAF_BAN_RESPONSE` and bans the client's netblock for a
clear sign of attack, as a `ban` rule's match does. A trap path is matched
against the path a `path` rule sees: as the client sent it, before any
decoding and without the query, the whole of it, character for character.
`/wp-login.php` matches `/wp-login.php?redirect_to=x`, but not
`/wp-login.php/`, `/WP-LOGIN.PHP`, `/blog/wp-login.php` or `/%77p-login.php`.
A client in `SWWAF_ALLOW_NETS` is not checked.
- Bans a client for a clear sign of attack, as "Bans" in [`SPEC.md`](SPEC.md)
describes: the first such ban lasts `SWWAF_ATTACK_BAN_DURATION`, seven days by
default, and any request from the netblock while it lasts makes it permanent.
@@ -190,8 +203,22 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
sign of attack bans it permanently at once. Such a ban covers the same
netblock as a ban for a broken limit, does not set the client's counters back
to zero, and does not make the netblock's next ban for a broken limit longer.
Its notes give the id and the target of the rule that matched in place of the
limit.
Its notes give the id and the target of the rule that matched, or the trap
path asked for, in place of the limit.
- Bans a client that `smallwebwaf` refused more than
`SWWAF_ERROR_BURST_THRESHOLD` times within a minute, 30 by default, after a
match of a `block` or `ban` rule or a trap path, or for a missing or wrong
token at one of its own endpoints, as a broken rate limit bans it. Each such
refusal is counted once it has been answered, in two buckets of a minute, as
the rate limits count requests. The refusal that takes the client over the
threshold breaks the error burst; it is answered as any other such refusal is,
and the client's next request is refused under the ban. The app's own answers,
such as its `401` and `404`, are not counted. The threshold is the same for
every client, whatever percentage of the rate limits a biased threshold or a
reputation source gives it. A client in `SWWAF_ALLOW_NETS` is not counted, and
one in `SWWAF_RATE_LIMIT_EXEMPT_NETS` is. The ban's notes give `refusals` as
what the limit is on, the threshold as the limit, and the refusals counted in
the minute.
- Looks up the AS number and country of every client through GeoJS, or in the
IPinfo Lite database file while `SWWAF_LOOKUP_SOURCE` is `file`, after the
static lists and bans, unless `SWWAF_LOOKUP_SOURCE` is `off` (see "Country and
@@ -242,44 +269,50 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
- Checks the client with AbuseIPDB while `SWWAF_ABUSEIPDB_KEY` is set, after the
DNSBL zones and before the rate limits (see "AbuseIPDB" below), by the scores
it keeps. Only a client whose history counts an offence is checked, so far one
that has broken a rate limit or a byte limit, matched a ban rule, or had a
request refused by a block rule, and only in the background, so that no
request waits for AbuseIPDB. A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE` is
a hit, and `SWWAF_REPUTATION_ACTION` does with its client what it does with
one a DNSBL zone's verdict lists. The request's log line names AbuseIPDB, and
it raises an alert. A client a blocklist or a DNSBL zone refuses, or the
CrowdSec decision list bans, is not checked.
that has broken a rate limit, a byte limit or the error burst, matched a ban
rule, asked for a trap path, or had a request refused by a block rule or for a
missing or wrong token, and only in the background, so that no request waits
for AbuseIPDB. A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE` is a hit, and
`SWWAF_REPUTATION_ACTION` does with its client what it does with one a DNSBL
zone's verdict lists. The request's log line names AbuseIPDB, and it raises an
alert. A client a blocklist or a DNSBL zone refuses, or the CrowdSec decision
list bans, is not checked.
- Checks the client's own address against the static lists, the three netblock
settings below, before anything else, its lookup included. A client in
`SWWAF_ALLOW_NETS` skips bans, the country lists, the blocklists, the CrowdSec
decision list, the DNSBL zones, AbuseIPDB, the rate limits, the byte limits
and the rule files, and is not looked up; the timeouts and size limits still
apply. A client in `SWWAF_DENY_NETS` is refused with `SWWAF_BAN_RESPONSE`
before its body is read, and the request is not counted for the rate limits;
an address in `SWWAF_ALLOW_NETS` too is let through. A client in
`SWWAF_RATE_LIMIT_EXEMPT_NETS` is neither counted nor refused by the rate
limits, and has no bytes counted by the byte limits; the country lists, the
rule files and bans still apply to it.
decision list, the DNSBL zones, AbuseIPDB, the rate limits, the byte limits,
the trap paths, the rule files and the error burst, and is not looked up; the
timeouts and size limits still apply. A client in `SWWAF_DENY_NETS` is refused
with `SWWAF_BAN_RESPONSE` before its body is read, and the request is not
counted for the rate limits; an address in `SWWAF_ALLOW_NETS` too is let
through. A client in `SWWAF_RATE_LIMIT_EXEMPT_NETS` is neither counted nor
refused by the rate limits, and has no bytes counted by the byte limits; the
country lists, the trap paths, the rule files, the error burst and bans still
apply to it.
- In `observe` mode, with `SWWAF_MODE=observe`, refuses none of the requests
that `SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, the CrowdSec
decision list, a DNSBL zone's verdict, AbuseIPDB's score, a rate limit or a
rule would refuse: it passes them to the app, and their log lines name what
`enforce` mode would have done (see `would_action` in "Request log" below).
The checks run, and requests and bytes are counted, as in `enforce` mode, with
three differences: neither a broken rate limit or byte limit, a `ban` rule nor
the CrowdSec decision list makes a ban; a broken limit does not set the
client's counters back to zero, so each request over a rate limit is logged as
one that would be refused, and each whose bytes keep the client over a byte
limit as breaking it; and a request under a ban does not make it permanent. As
in `enforce` mode, the bytes counted are only those of the requests `enforce`
mode would have passed to the app. A ban it would have made, or made
permanent, raises the alert `enforce` mode would have raised, marked as what
would have happened (see "Alerts" below). The bans in `bans.json` are kept,
and refuse requests again when `smallwebwaf` next runs in `enforce` mode, as
long as they last. The timeouts and size limits still apply, since they
protect `smallwebwaf` and the app themselves, and a request for one of
`smallwebwaf`'s own endpoints without its token is still answered `401`. It is
for trying a configuration before enforcing it.
decision list, a DNSBL zone's verdict, AbuseIPDB's score, a rate limit, a trap
path or a rule would refuse: it passes them to the app, and their log lines
name what `enforce` mode would have done (see `would_action` in "Request log"
below). The checks run, and requests, bytes and refusals are counted, as in
`enforce` mode, with three differences: neither a broken rate limit, byte
limit or error burst, a `ban` rule, a trap path nor the CrowdSec decision list
makes a ban; a broken limit does not set the client's counters back to zero,
so each request over a rate limit is logged as one that would be refused, each
whose bytes keep the client over a byte limit as breaking it, and each refusal
that keeps it over the error burst as breaking that; and a request under a ban
does not make it permanent. As in `enforce` mode, the bytes counted are only
those of the requests `enforce` mode would have passed to the app, and the
refusals counted for the error burst only those it would have made: a request
it would have refused before it reached an endpoint is not counted for its
token. A ban it would have made, or made permanent, raises the alert `enforce`
mode would have raised, marked as what would have happened (see "Alerts"
below). The bans in `bans.json` are kept, and refuse requests again when
`smallwebwaf` next runs in `enforce` mode, as long as they last. The timeouts
and size limits still apply, since they protect `smallwebwaf` and the app
themselves, and a request for one of `smallwebwaf`'s own endpoints without its
token is still answered `401`. It is for trying a configuration before
enforcing it.
- Answers `GET /_smallwebwaf/healthz` itself with `200` and `ok`, before any
check and without asking the app, for the image's health check.
- Answers `GET /_smallwebwaf/metrics` with its metrics (see "Metrics" below) for
@@ -385,8 +418,8 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
- `SWWAF_RESPONSE_MAX_BYTES` (default `5G`): the largest response body.
- `SWWAF_ALLOW_NETS` (default empty): netblocks whose clients skip bans, the
country lists, the blocklists, the CrowdSec decision list, the DNSBL zones,
AbuseIPDB, the rate limits, the byte limits and the rule files, such as your
monitoring or your own networks.
AbuseIPDB, the rate limits, the byte limits, the trap paths, the rule files
and the error burst, such as your monitoring or your own networks.
- `SWWAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients the
rate limits and the byte limits do not apply to, such as a machine that talks
to the app all day.
@@ -530,21 +563,21 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
- `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone, or a
check with AbuseIPDB, may take before it fails.
- `SWWAF_BAN_RESPONSE` (default `403`): how a refused client is answered, one
that is banned, breaks a rate limit, matches a `ban` rule, is in
`SWWAF_DENY_NETS`, comes from a refused country, is in a blocklist while
`SWWAF_BLOCKLIST_ACTION` is `deny` or is listed by a DNSBL zone or AbuseIPDB
while `SWWAF_REPUTATION_ACTION` is `deny`: `403`, `429`, or `close` to close
the connection without an answer. Behind traefik, `close` does not leave the
client unanswered: traefik answers `502`, as it does whenever its backend
drops a connection. A `block` rule always answers `403`.
that is banned, breaks a rate limit, matches a `ban` rule, asks for a trap
path, is in `SWWAF_DENY_NETS`, comes from a refused country, is in a blocklist
while `SWWAF_BLOCKLIST_ACTION` is `deny` or is listed by a DNSBL zone or
AbuseIPDB while `SWWAF_REPUTATION_ACTION` is `deny`: `403`, `429`, or `close`
to close the connection without an answer. Behind traefik, `close` does not
leave the client unanswered: traefik answers `502`, as it does whenever its
backend drops a connection. A `block` rule always answers `403`.
- `SWWAF_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken rate
limit or byte limit.
- `SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): a rate limit or byte limit
broken again within this time after a ban ended, other than one for a clear
sign of attack or for CrowdSec's decision, bans for three times as long as
that ban.
- `SWWAF_MAX_BAN_DURATION` (default `7d`): a ban for a broken rate limit or byte
limit that would be longer is permanent instead.
limit, byte limit or error burst.
- `SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): a rate limit, byte limit or
error burst broken again within this time after a ban ended, other than one
for a clear sign of attack or for CrowdSec's decision, bans for three times as
long as that ban.
- `SWWAF_MAX_BAN_DURATION` (default `7d`): a ban for a broken rate limit, byte
limit or error burst that would be longer is permanent instead.
- `SWWAF_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign of
attack.
- `SWWAF_MAX_BANS` (default `5000`): the most bans `smallwebwaf` made that are
@@ -588,6 +621,18 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
rule files. A directory that does not exist stops the start.
- `SWWAF_RULES_ENABLED` (default `true`): `false` reads no rule file, and checks
no request against one.
- `SWWAF_TRAP_PATHS` (default empty): paths the app never serves and only
scanners ask for, such as `/wp-login.php,/xmlrpc.php` in front of gitea; a
request for one bans its client for a clear sign of attack, as a `ban` rule
does, with or without rule files (see "What it does so far" above). A path
that does not start with `/`, or holds a `?`, which no path a `path` rule sees
holds, stops the start.
- `SWWAF_ERROR_BURST_THRESHOLD` (default `30`): the most requests of a client in
a minute that `smallwebwaf` may refuse after a rule file match or a trap path,
or for a missing or wrong token; one more breaks the error burst, and bans the
client as a broken rate limit does (see "What it does so far" above). A client
trying one probe or token after another is refused many times a minute, a
person rarely more than a few times. `off` switches it off.
- `SWWAF_LOG_REMOTE_URL` (default unset): a syslog server that every line on
stdout is also sent to, as `syslog+udp://`, `syslog+tcp://` or `syslog+tls://`
with a host and a port, such as `syslog+tls://logs.example:6514`. Unset or
@@ -671,18 +716,19 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
Durations are in Go's syntax, with `d` for days (`90s`, `15m`, `7d`). Sizes are
bytes, with an optional `K`, `M` or `G`, which are powers of 1024 (`1K` is 1024
bytes). Rate limits and the anomaly thresholds on requests are whole numbers of
requests, and byte limits and the anomaly thresholds on bytes are sizes.
Netblocks are in CIDR form, and a bare address stands for itself alone.
Countries are the two-letter codes ISO 3166-1 assigns today, and `xk` for
Kosovo, in either case (`de` and `DE` are the same); any other code, such as
`nk` (North Korea is `kp`) or the withdrawn `su`, stops the start, and so does a
code on both country lists. AS numbers are `AS` and the number, in either case.
Percentages are whole numbers from 0 to 100, and an entry of a list of them is
an AS number or a country, `:` and a percentage; an AS number or a country
listed twice in one of them stops the start. `off` switches a timeout, a size
limit, a rate limit, a byte limit, an anomaly threshold, `SWWAF_ALERT_COOLDOWN`
or `SWWAF_ALERT_MAX_PER_HOUR` off; `SWWAF_IPV6_GROUP_PREFIX`,
bytes). Rate limits, `SWWAF_ERROR_BURST_THRESHOLD` and the anomaly thresholds on
requests are whole numbers of requests, and byte limits and the anomaly
thresholds on bytes are sizes. Netblocks are in CIDR form, and a bare address
stands for itself alone. Countries are the two-letter codes ISO 3166-1 assigns
today, and `xk` for Kosovo, in either case (`de` and `DE` are the same); any
other code, such as `nk` (North Korea is `kp`) or the withdrawn `su`, stops the
start, and so does a code on both country lists. AS numbers are `AS` and the
number, in either case. Percentages are whole numbers from 0 to 100, and an
entry of a list of them is an AS number or a country, `:` and a percentage; an
AS number or a country listed twice in one of them stops the start. `off`
switches a timeout, a size limit, a rate limit, a byte limit, an anomaly
threshold, `SWWAF_ERROR_BURST_THRESHOLD`, `SWWAF_ALERT_COOLDOWN` or
`SWWAF_ALERT_MAX_PER_HOUR` off; `SWWAF_IPV6_GROUP_PREFIX`,
`SWWAF_MAX_TRACKED_CLIENTS`, `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES`,
`SWWAF_LOOKUP_TIMEOUT`, `SWWAF_UNKNOWN_LIMIT_PERCENT`,
`SWWAF_BLOCKLIST_REFRESH`, `SWWAF_ABUSEIPDB_MIN_SCORE`,
@@ -781,20 +827,20 @@ which every line has.
refused because its client is in `SWWAF_DENY_NETS`, in a blocklist while
`SWWAF_BLOCKLIST_ACTION` is `deny`, or listed by a DNSBL zone or AbuseIPDB
while `SWWAF_REPUTATION_ACTION` is `deny`, `banned` for one refused because a
ban covers its client, or because it matched a `ban` rule or the CrowdSec
decision list lists its client, either of which bans its client,
`country_denied` for one refused for its client's country, `rate_limited` for
one that broke a rate limit and banned its client, `rule_blocked` for one a
`block` rule refused, `too_large` for a request or response over its size
limit, `timed_out` for one that ran out of time, `upstream_error` when the app
could not be reached or its answer broke off, and `admin` for one
`smallwebwaf` answered at its own endpoint.
ban covers its client, or because it matched a `ban` rule, asked for a trap
path or the CrowdSec decision list lists its client, each of which bans its
client, `country_denied` for one refused for its client's country,
`rate_limited` for one that broke a rate limit and banned its client,
`rule_blocked` for one a `block` rule refused, `too_large` for a request or
response over its size limit, `timed_out` for one that ran out of time,
`upstream_error` when the app could not be reached or its answer broke off,
and `admin` for one `smallwebwaf` answered at its own endpoint.
- `would_action` is there in `observe` mode for a request that
`SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, the CrowdSec
decision list, a DNSBL zone's verdict, AbuseIPDB's score, a rate limit or a
rule would have refused in `enforce` mode, and names the action that refusal
would have had: `denied`, `banned`, `country_denied`, `rate_limited` or
`rule_blocked`. `action` then names what was done: `forward` for a request
decision list, a DNSBL zone's verdict, AbuseIPDB's score, a rate limit, a trap
path or a rule would have refused in `enforce` mode, and names the action that
refusal would have had: `denied`, `banned`, `country_denied`, `rate_limited`
or `rule_blocked`. `action` then names what was done: `forward` for a request
passed to the app, and another action, such as `too_large`, for one a size or
time limit refused.
- `limit_percent` is there for a request the rate limits count whose client a
@@ -822,13 +868,15 @@ which every line has.
counted before it.
- `rule_ids` is there for a request that matched rules of the rule files, and
lists their ids in the order they matched, up to the one that refused it.
- `limit_hit` is there for a request that broke a rate limit, or whose bytes
broke a byte limit, and names the window whose limit it went over as `counts`
names it: `minute`, `hour` or `day` for a rate limit, and `minute_bytes`,
`hour_bytes` or `day_bytes` for a byte limit, the shortest if it went over
several. `offence` is then `limit`. A request whose bytes broke a byte limit
is not refused: its `action` is what it would have been otherwise, such as
`forward`.
- `limit_hit` is there for a request that broke a rate limit or the error burst,
or whose bytes broke a byte limit, and names the window whose limit it went
over as `counts` names it: `minute`, `hour` or `day` for a rate limit, and
`minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit, the shortest if
it went over several; or `error_burst` for the error burst. `offence` is then
`limit`. A request whose bytes broke a byte limit is not refused: its `action`
is what it would have been otherwise, such as `forward`. Nor is one that broke
the error burst refused for that: its `action` is that of the refusal counted,
such as `rule_blocked` or `admin`.
- `reputation` is there for a request whose client a blocklist, the CrowdSec
decision list or a DNSBL zone's verdict lists, or whose AbuseIPDB score is a
hit, and gives the URLs of the blocklists that list it, in the order
@@ -1101,33 +1149,42 @@ which are listed by scope first, and the copies of the lists, listed by URL,
with times in UTC.
- `bans.json`: every ban with its notes, indented to be read. A permanent ban's
`expires` is `null`. A ban's `cause` is `limit` for a broken rate limit or
byte limit, `attack` for a clear sign of attack or `crowdsec` for a client the
CrowdSec decision list lists, for a ban `smallwebwaf` made, and `admin` for
one you made or keep. Its `reason` is a short text: for a ban `smallwebwaf`
made, the limit broken, such as `requests per minute over the limit of 1000`
or `bytes per hour over the limit of 21474836480`, the rule that matched, such
as `matched the rule env-file`, or the scenario that made CrowdSec's decision,
such as `CrowdSec's decision for crowdsecurity/ssh-bf`; for yours, what you
wrote. Its `lifted` is when you lifted it, and is left out until you do. The
`kind` in the notes of a ban for a broken limit is `requests` or `bytes`, what
the limit is on. For a limit a biased threshold lowered, the reason and the
notes' `limit` give the lowered limit, and the notes' `limit_percent` and
`limit_percent_setting` the client's percentage of that kind of limit and the
setting that gave it. The notes' `reputation` gives each blocklist, the
CrowdSec decision list, each DNSBL zone or AbuseIPDB that listed the client
when the ban was made, as its `source`, named and ordered as in the request
log's `reputation`, with AbuseIPDB's `score` of the client. It is left out
when none did, and the example below shows it.
`expires` is `null`. A ban's `cause` is `limit` for a broken rate limit, byte
limit or error burst, `attack` for a clear sign of attack or `crowdsec` for a
client the CrowdSec decision list lists, for a ban `smallwebwaf` made, and
`admin` for one you made or keep. Its `reason` is a short text: for a ban
`smallwebwaf` made, the limit broken, such as
`requests per minute over the limit of 1000`,
`bytes per hour over the limit of 21474836480` or
`refusals per minute over the limit of 30`, the rule that matched, such as
`matched the rule env-file`, the trap path asked for, such as
`asked for the trap path /wp-login.php`, or the scenario that made CrowdSec's
decision, such as `CrowdSec's decision for crowdsecurity/ssh-bf`; for yours,
what you wrote. Its `lifted` is when you lifted it, and is left out until you
do. The `kind` in the notes of a ban for a broken limit is `requests`, `bytes`
or `refusals`, for the error burst, what the limit is on. The notes of a ban
for a clear sign of attack give the `rule_id` and the `target` of the rule
that matched, or the `trap_path` asked for. For a limit a biased threshold
lowered, the reason and the notes' `limit` give the lowered limit, and the
notes' `limit_percent` and `limit_percent_setting` the client's percentage of
that kind of limit and the setting that gave it. The notes' `reputation` gives
each blocklist, the CrowdSec decision list, each DNSBL zone or AbuseIPDB that
listed the client when the ban was made, as its `source`, named and ordered as
in the request log's `reputation`, with AbuseIPDB's `score` of the client. It
is left out when none did, and the example below shows it.
- `clients.json`: each client's two buckets of requests in the minute, the hour
and the day, its two buckets of bytes in each, `minute_bytes`, `hour_bytes`
and `day_bytes`, and its history: when it was first and last seen, its AS
number, AS name and country as last looked up and when the lookup gave them,
its requests, how many were forwarded and how many refused (one `smallwebwaf`
answered at its own endpoints is neither, unless it was refused with `401` for
a missing or wrong token), the body bytes in each direction, its responses by
status class and its offences by kind. Each client is on a line of its own, so
`grep` shows everything about one.
and `day_bytes`, its two buckets of refusals in the minute, which the error
burst counts, `minute_refusals`, and its history: when it was first and last
seen, its AS number, AS name and country as last looked up and when the lookup
gave them, its requests, how many were forwarded and how many refused (one
`smallwebwaf` answered at its own endpoints is neither, unless it was refused
with `401` for a missing or wrong token), the body bytes in each direction,
its responses by status class and its offences by kind: `limit` for a broken
rate limit, byte limit or error burst, `attack` for a clear sign of attack,
`rule_blocked` for a request a `block` rule refused and `token_refused` for
one refused for a missing or wrong token. Each client is on a line of its own,
so `grep` shows everything about one.
- `lookups.json`: GeoJS's answers, one to a line, each with the client's AS
number, AS name and country, when GeoJS gave it and when it was last used.
- `reputation.json`: each list fetched from a URL (see "Blocklists" and
@@ -1243,19 +1300,19 @@ and the line and column where Go's JSON decoder gives them; so does a state
directory `smallwebwaf` cannot write. So does an entry without a field it needs,
named with the entry's place in the file: a ban's `netblock`, `start` or
`expires`, which is `null` for a permanent ban; a client's `client`, or the
`start` of a window in which it has requests or bytes; an answer's `client`,
`country`, which is `""` for a client GeoJS cannot place, or `answered`; a
cooldown's `event` or `sent`; an alert waiting's `event` or `time`; an anomaly
counter's `netblock`, unless it counts an AS number or the whole service, its
`asn`, for an AS number, its `name`, for a named netblock, or the `start` of a
window in which it has requests or bytes; a list's `url`, `fetched` or `lines`,
which is `[]` for an empty list; a verdict's `zone`, `client`, `listed`, which
is `false` for a client the zone does not list, or `fetched`. So does a ban
whose `cause` is not `limit`, `attack`, `admin` or `crowdsec`, alerts waiting
for a destination that is not `webhook`, `slack` or `ntfy`, an anomaly counter
whose `scope` is not `client`, `net`, `asn`, `total` or `watch`, and a copy of a
list with a line that would make its fetch fail. An answer's `asn` or `as_name`
left out reads as empty.
`start` of a window in which it has requests, bytes or refusals; an answer's
`client`, `country`, which is `""` for a client GeoJS cannot place, or
`answered`; a cooldown's `event` or `sent`; an alert waiting's `event` or
`time`; an anomaly counter's `netblock`, unless it counts an AS number or the
whole service, its `asn`, for an AS number, its `name`, for a named netblock, or
the `start` of a window in which it has requests or bytes; a list's `url`,
`fetched` or `lines`, which is `[]` for an empty list; a verdict's `zone`,
`client`, `listed`, which is `false` for a client the zone does not list, or
`fetched`. So does a ban whose `cause` is not `limit`, `attack`, `admin` or
`crowdsec`, alerts waiting for a destination that is not `webhook`, `slack` or
`ntfy`, an anomaly counter whose `scope` is not `client`, `net`, `asn`, `total`
or `watch`, and a copy of a list with a line that would make its fetch fail. An
answer's `asn` or `as_name` left out reads as empty.
While it runs, `smallwebwaf` watches `SWWAF_STATE_DIR` and takes in your edit of
a state file as soon as you save it: what the file then holds replaces what
@@ -1406,9 +1463,11 @@ scraped, and keeps this one as `exported_instance` unless the scrape sets
from then on, as histograms; `smallwebwaf_requests_in_flight`: the requests
under way.
- `smallwebwaf_rate_limit_hits_total` by `window`, `minute`, `hour` or `day`,
and `kind`, `requests` for a rate limit or `bytes` for a byte limit,
and `kind`, `requests` for a rate limit, `bytes` for a byte limit or
`refusals` for the error burst, whose window is `minute`,
`smallwebwaf_size_and_time_limit_hits_total` by `limit`, the setting whose
limit was passed, `smallwebwaf_offences_total` by `kind`, and
limit was passed, `smallwebwaf_offences_total` by `kind`, `limit`, `attack`,
`rule_blocked` or `token_refused`, as `clients.json` counts them, and
`smallwebwaf_bans_made_total` by `cause`, `limit`, `attack`, `admin` or
`crowdsec`, `admin` for the bans you add through `POST /_smallwebwaf/bans`,
and those whose `cause` is `admin` that you add to `bans.json` while
@@ -1521,7 +1580,8 @@ or lift is written to `bans.json` `SWWAF_STATE_WRITE_DELAY` later. Refusals, and
the answers to requests that cannot be read, are plain text.
A request without the token, or with another, such as the metrics token, is
answered `401`, in `observe` mode too. While the token is unset, each of these
answered `401`, in `observe` mode too, and counts toward the error burst, as one
for the metrics without theirs does. While the token is unset, each of these
answers `404`, as does any request under `/_smallwebwaf/` that is not for one of
its endpoints. Like the metrics, these requests go through every check any other
request goes through, and are answered where another would be passed to the app:
@@ -1679,13 +1739,14 @@ For each request `smallwebwaf`:
- picks the client's limit percentage from those;
- checks the minute, hour and day request counters against the limits, and bans
the client if it breaks one;
- checks the request against the rule files and the Core Rule Set, and bans the
client at once for a clear sign of attack;
- checks the request against the trap paths, the rule files and the Core Rule
Set, and bans the client at once for a clear sign of attack;
- forwards it to the app and streams the response back, within the size and time
limits;
- counts the bytes and any refusal by the rule files or the Core Rule Set, bans
the client if it broke a limit, updates its history and the anomaly counters,
sends any alerts that are due, and writes the log line.
- counts the bytes and any refusal by the trap paths, the rule files or the Core
Rule Set or for a missing or wrong token, bans the client if it broke a limit,
updates its history and the anomaly counters, sends any alerts that are due,
and writes the log line.
A minimal deployment is the app's own Dockerfile, built on the `smallwebwaf`
image, with no setting. That image is built on Ubuntu 26.04 LTS, the newest
@@ -2054,14 +2115,16 @@ alerts, the metrics nor `reputation.json` hold it. Given as a file, with
request is refused before anything reaches the app: for `SWWAF_DENY_NETS`, for
a ban, for the country lists, for a blocklist, for the CrowdSec decision list,
which bans the client, for a DNSBL zone's verdict, for AbuseIPDB's score, for
a rate limit, which bans the client, for a `block` or `ban` rule, the latter
banning the client, and for an announced body over the size limit; in
`observe` mode, only for the size limit, with what it would have refused for
noted in the log line. A request under `/_smallwebwaf/` that `check` lets
through is answered by `answerAdmin` instead of reaching the app. Once the
answer to a request passed to the app has ended, `countBytes` counts its bytes
for the byte limits, and once any request but the health check has ended,
`countAnomalies` counts it for the anomaly thresholds.
a rate limit, which bans the client, for a trap path, which bans the client,
for a `block` or `ban` rule, the latter banning the client, and for an
announced body over the size limit; in `observe` mode, only for the size
limit, with what it would have refused for noted in the log line. A request
under `/_smallwebwaf/` that `check` lets through is answered by `answerAdmin`
instead of reaching the app. Once the answer to a request passed to the app
has ended, `countBytes` counts its bytes for the byte limits, and once any
request but the health check has ended, `countRefusal` counts it for the error
burst if it was refused after a rule file match or a trap path or for its
token, and `countAnomalies` counts it for the anomaly thresholds.
- `internal/metrics`: the metrics, counted as the other parts tell it what
happened, and served in the Prometheus text format.
- `internal/bans`: the ban ledger: each netblock's bans with their notes, how
@@ -2083,9 +2146,9 @@ alerts, the metrics nor `reputation.json` hold it. Given as a file, with
tells which zones' verdicts list an address; and checks clients with AbuseIPDB
in the background, keeps their scores and the checks spent today, and tells
whether a client's score is a hit.
- `internal/ratelimit`: the table of clients: counts each client's requests and
bytes, tells when they take it over a rate limit or a byte limit, and keeps
each client's history.
- `internal/ratelimit`: the table of clients: counts each client's requests,
bytes and refusals, tells when they take it over a rate limit, a byte limit or
the error burst, and keeps each client's history.
- `internal/anomaly`: the anomaly counters: counts each request and its bytes
per client, per netblock around a client, per AS number, for the whole service
and per named netblock, in the buckets `internal/ratelimit` counts in, and