Trap paths, and the error burst banning a client refused too often (closes #115)
check / check (push) Waiting to run
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user