The Core Rule Set, run by Coraza, on each request's method, URL and headers (closes #25)
check / check (push) Waiting to run

Coraza v3.8.1 runs the Core Rule Set 4.25.0 (coraza-coreruleset v4.25.0)
after the rule files, with the six changes and the default
SWWAF_WAF_DISABLED_RULES that SPEC.md gives; no body, no response.
SWWAF_WAF_MODE, SWWAF_WAF_PARANOIA_LEVEL, SWWAF_WAF_ANOMALY_THRESHOLD and
SWWAF_WAF_EXEMPT_PATHS as specified. In block mode a match is refused with
403, an offence counted toward the error burst; in detect mode it is let
through. Both log waf_rule_ids, waf_score and duration_waf, raise
waf_block, and count smallwebwaf_waf_matches_total.

Judgement call: waf_block is raised in block mode too.
Deviation: no engine-error path; with no body read, Coraza cannot fail.

Model: opus-5-5
This commit is contained in:
2026-10-08 05:44:13 +00:00
parent 54779f08de
commit 22b52dfd6d
20 changed files with 1614 additions and 229 deletions
+241 -144
View File
@@ -32,39 +32,43 @@ 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. 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
is the last stage: attack detection with the OWASP Core Rule Set, run by Coraza,
on the method, the URL and the headers of each request, 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 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
sign of attack, refuses a request the Core Rule Set takes for an attack, bans a
client it refuses again and again within a minute for a rule, a trap path, the
Core Rule Set 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
passes on the requests it would refuse, logging what it would have done with
them. It comes as the image the app's own image is built on. The rest of the
design comes after that, in the order of the build order in
[`SPEC.md`](SPEC.md). The survey of existing tools that led to the design is in
[`EVALUATION.md`](EVALUATION.md).
you name one, for each ban it makes or makes permanent, for a request the Core
Rule Set takes for an attack, 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 passes on the requests it would
refuse, logging what it would have done with them. It comes as the image the
app's own image is built on. The Core Rule Set reads no request body yet:
`SWWAF_WAF_BODY_LIMIT`, which switches that on, comes with
https://git.eeqj.de/sneak/smallwebwaf/issues/116. The rest of the design comes
after that, in the order of the build order in [`SPEC.md`](SPEC.md). The survey
of existing tools that led to the design is in [`EVALUATION.md`](EVALUATION.md).
## Getting started
@@ -121,13 +125,14 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
bans the client. A request whose path starts with one of
`SWWAF_RATE_LIMIT_EXEMPT_PATHS`, as that setting below describes, is neither
counted nor refused by the rate limits; the static lists, bans, the country
lists and the rule files still apply to it. A client is one IPv4 address, or
one IPv6 group, the netblock of `SWWAF_IPV6_GROUP_PREFIX` its address is in, a
/64 by default, since one abuser usually holds a whole /64. Each window is
counted in two fixed buckets, the earlier one weighted by how much of it the
window still covers. At most `SWWAF_MAX_TRACKED_CLIENTS` clients are kept,
20,000 by default, the least recently seen dropped first, with their history,
and a restart gives no client a fresh allowance (see "State files" below).
lists, the rule files and the Core Rule Set still apply to it. A client is one
IPv4 address, or one IPv6 group, the netblock of `SWWAF_IPV6_GROUP_PREFIX` its
address is in, a /64 by default, since one abuser usually holds a whole /64.
Each window is counted in two fixed buckets, the earlier one weighted by how
much of it the window still covers. At most `SWWAF_MAX_TRACKED_CLIENTS`
clients are kept, 20,000 by default, the least recently seen dropped first,
with their history, and a restart gives no client a fresh allowance (see
"State files" below).
- Counts each client's bytes over a minute, an hour and a day, in the same way:
once a request passed to the app has ended, the body bytes of its answer, of
the request, or of both, as `SWWAF_BYTES_COUNT` says. For a WebSocket, or any
@@ -196,6 +201,40 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
`/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.
- Inspects each request with the OWASP Core Rule Set 4.25.0, run by Coraza,
after the rule files, unless `SWWAF_WAF_MODE` is `off`: its method, its URL
with the query, and its headers, but no request body and no response. Each of
its rules that matches adds to the request's anomaly score, up to the paranoia
level `SWWAF_WAF_PARANOIA_LEVEL` sets. A score at or over
`SWWAF_WAF_ANOMALY_THRESHOLD`, 5 by default, is a match: in `block` mode, the
default, the request is refused with `403`, and in `detect` mode it goes on to
the app. Either way its log line names the rules and the score (see
`waf_rule_ids` and `waf_score` in "Request log" below), and it raises a
`waf_block` alert. A refusal bans no one by itself, since the Core Rule Set
takes some ordinary requests for attacks, but it is an offence the client's
history counts, and it counts toward the error burst; a match in `detect` mode
is neither. A request a rule file refuses, one for a path
`SWWAF_WAF_EXEMPT_PATHS` exempts, and one from a client in `SWWAF_ALLOW_NETS`
are not inspected. `smallwebwaf` changes the Core Rule Set in six ways, so
that gitea's ordinary requests get through, and no setting undoes them:
- `PUT`, `PATCH` and `DELETE` are allowed besides `GET`, `HEAD`, `POST` and
`OPTIONS`; any other method stays refused.
- The headers `Expect` and `Content-Encoding` are allowed; the others the
Core Rule Set refuses, such as `Proxy` and `Content-Range`, stay refused.
- The query parameter `redirect_uri` is not checked for a URL naming an IP
address or `localhost` (rules 931100 and 934110), which Git Credential
Manager, git-credential-oauth and tea ask to be sent back to.
- The query parameters `path`, `files`, `skip-to`, `sub_path`, `ref`, `sha`,
`branch`, `workflow`, `artifactName` and `redirect_to` are not checked
against the lists of system files (930120), shell paths (932160) and
command names (932260), so that a file such as `.gitignore` or a branch
such as `docker-build` gets through there. So does what only these rules
refuse, such as `|cat /etc/passwd`; path traversal and SQL injection are
still refused there.
- The cookies `gitea_flash` and `redirect_to` are not read, and `Referer` is
not checked for a Unix command given without arguments (932340) or for
Java starting a process (944110); it is checked by every other rule.
- Responses are not inspected.
- 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.
@@ -207,18 +246,18 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
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.
match of a `block` or `ban` rule, a trap path or the Core Rule Set, 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
@@ -270,49 +309,50 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
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, 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.
rule, asked for a trap path, or had a request refused by a block rule, by the
Core Rule Set 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,
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
the trap paths, the rule files, the Core Rule Set 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 Core Rule Set, 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, 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.
path, a rule or the Core Rule Set 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
@@ -331,17 +371,18 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
- Sends every line it writes on stdout to a syslog server as well, while
`SWWAF_LOG_REMOTE_URL` names one (see "Sending the log to a syslog server"
below).
- Sends an alert for each ban it makes or makes permanent, for a count over an
anomaly threshold, for a client a blocklist, the CrowdSec decision list, a
DNSBL zone or AbuseIPDB lists, for GeoJS failing, a list it cannot fetch, the
CrowdSec decision list among them, 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, holding back repeats and, past an hourly limit, rolling the rest
into one summary, to each destination you name: as a JSON object to the
webhook `SWWAF_ALERT_WEBHOOK_URL` names, as a message to the Slack incoming
webhook `SWWAF_ALERT_SLACK_WEBHOOK_URL` names, and as a message to the ntfy
topic `SWWAF_ALERT_NTFY_URL` names (see "Alerts" below).
- Sends an alert for each ban it makes or makes permanent, for a match of the
Core Rule Set, for a count over an anomaly threshold, for a client a
blocklist, the CrowdSec decision list, a DNSBL zone or AbuseIPDB lists, for
GeoJS failing, a list it cannot fetch, the CrowdSec decision list among them,
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, holding back repeats and,
past an hourly limit, rolling the rest into one summary, to each destination
you name: as a JSON object to the webhook `SWWAF_ALERT_WEBHOOK_URL` names, as
a message to the Slack incoming webhook `SWWAF_ALERT_SLACK_WEBHOOK_URL` names,
and as a message to the ntfy topic `SWWAF_ALERT_NTFY_URL` names (see "Alerts"
below).
- Counts requests and their bytes over a minute and an hour, per client, per
netblock around a client, per AS number, for the whole service and per named
netblock, and sends an `anomaly` alert for a count over the anomaly threshold
@@ -621,6 +662,35 @@ 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_WAF_MODE` (default `block`): what the Core Rule Set does with a match:
`block` refuses it with `403`, `detect` lets it through, logged and alerted,
and `off` inspects no request (see "What it does so far" above).
- `SWWAF_WAF_PARANOIA_LEVEL` (default `1`): the Core Rule Set's paranoia level,
from 1 to 4. Each level up runs more of its rules, which find more attacks and
take more ordinary requests for them.
- `SWWAF_WAF_ANOMALY_THRESHOLD` (default `5`): the anomaly score at which a
request is a match. A rule the Core Rule Set rates critical adds 5, so by
default one such rule is enough. `off` makes no request a match; the scores
are still logged.
- `SWWAF_WAF_DISABLED_RULES` (default
`920340,920420,920440,920640,930130,930140`): the ids of the Core Rule Set's
rules to switch off, such as one that refuses ordinary requests of your app;
the request log names the rules a request matched in `waf_rule_ids`. The
default switches off the rules that refuse a request for the type of its body,
when it is missing or not on the Core Rule Set's short list (920340, 920420,
920640), for its file extension, such as `.sh` or `.sql` (920440), and for a
file or directory name in its path, such as `.git/`, `.gitignore`,
`Dockerfile`, `package.json` or an editor's settings directory (930130,
930140). In front of a code forge these refuse git over HTTP, container image
and package uploads, and views of ordinary files in a repository; the default
rule file bans the common probes for such files at the site root instead (see
"Rule files" below). A list given replaces the default, so include them in it;
set but empty, it switches no rule off. An item that is not a whole number
above zero stops the start, and the id of no rule switches nothing off.
- `SWWAF_WAF_EXEMPT_PATHS` (default empty): path prefixes whose requests the
Core Rule Set does not inspect, each starting with `/`, matched as
`SWWAF_RATE_LIMIT_EXEMPT_PATHS` matches its own: a request whose path holds
`..`, a backslash or an encoded slash is inspected whatever its prefix.
- `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
@@ -628,11 +698,12 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
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.
a minute that `smallwebwaf` may refuse after a rule file match, a trap path or
a Core Rule Set match, 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
@@ -678,8 +749,7 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
`SWWAF_INSTANCE_NAME`, which ntfy is sent in the title.
- `SWWAF_ALERT_EVENTS` (default
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
the events alerts are sent for. `waf_block` comes with the Core Rule Set;
nothing raises it yet.
the events alerts are sent for.
- `SWWAF_ALERT_COOLDOWN` (default `15m`): how long a repeat of an alert is held
back (see "Alerts" below).
- `SWWAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour;
@@ -727,8 +797,9 @@ 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`,
threshold, `SWWAF_WAF_ANOMALY_THRESHOLD`, `SWWAF_ERROR_BURST_THRESHOLD`,
`SWWAF_ALERT_COOLDOWN` or `SWWAF_ALERT_MAX_PER_HOUR` off;
`SWWAF_IPV6_GROUP_PREFIX`, `SWWAF_WAF_PARANOIA_LEVEL`,
`SWWAF_MAX_TRACKED_CLIENTS`, `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES`,
`SWWAF_LOOKUP_TIMEOUT`, `SWWAF_UNKNOWN_LIMIT_PERCENT`,
`SWWAF_BLOCKLIST_REFRESH`, `SWWAF_ABUSEIPDB_MIN_SCORE`,
@@ -831,18 +902,19 @@ which every line has.
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.
`rule_blocked` for one a `block` rule refused, `waf_blocked` for one the Core
Rule Set 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, 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.
path, a rule or the Core Rule Set would have refused in `enforce` mode, and
names the action that refusal would have had: `denied`, `banned`,
`country_denied`, `rate_limited`, `rule_blocked` or `waf_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
biased threshold, `SWWAF_BLOCKLIST_ACTION` for a blocklist that lists it, or
`SWWAF_REPUTATION_ACTION` for a DNSBL zone whose verdict lists it or an
@@ -868,6 +940,12 @@ 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.
- `waf_score` is there for a request the Core Rule Set inspected, and gives its
anomaly score, `0` for one no rule matched; `waf_rule_ids` lists the ids of
the rules that matched, in the order they ran, and is left out when none did.
A request is a match when its score is at or over
`SWWAF_WAF_ANOMALY_THRESHOLD`: in `block` mode its `action` is `waf_blocked`,
and in `detect` mode 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
@@ -900,7 +978,8 @@ which every line has.
- The timings are in milliseconds, to the microsecond. `duration_total` runs
from when the request's headers had been read to when its line is written, and
`duration_checks` over the same start to when the checks were done; the health
check runs none, and its line has no `duration_checks`.
check runs none, and its line has no `duration_checks`. `duration_waf`, the
part of the checks the Core Rule Set took, is there with `waf_score`.
`duration_upstream_connect`, `duration_upstream_first_byte` and
`duration_upstream_total` are there for a request passed to the app, and run
from when it was handed to the app: until there was a connection to it, new or
@@ -960,6 +1039,10 @@ it, as below. An alert is for one of these events, and is sent when
clear sign of attack, or a client the CrowdSec decision list lists.
- `permanent_ban`: a permanent ban it makes, or a ban for a clear sign of attack
that a request made permanent.
- `waf_block`: a request the Core Rule Set scored at or over
`SWWAF_WAF_ANOMALY_THRESHOLD`, in `block` mode, which refused it, and in
`detect` mode, which let it through, with `mode`, `detect`, in its `detail`;
in `observe` mode, `block` refused nothing either, and `mode` is `observe`.
- `anomaly`: a count of requests or bytes over an anomaly threshold, raised by
each request that ends with the count over it, in `observe` mode as in
`enforce` mode. It refuses and bans nothing.
@@ -1036,19 +1119,21 @@ is sent on one line:
- `client` is the address of the client whose request raised the alert, and
`netblock` the netblock of the ban, or for an `anomaly`, the netblock counted:
the client's own, the netblock around it or a named netblock, and none for an
AS number or the whole service, or for a `reputation_hit`, the client's own,
as `client_group` gives it; both are empty for `source_failure` and
`file_error`. `asn`, `as_name` and `country` are, for a ban, the client's as
the ban's notes give them when the alert is raised: empty, as in this alert,
when GeoJS had not answered about the client by then; for an `anomaly`, the
client's as the lookup gave them by the time its request ended; for a
`reputation_hit`, the client's as its request's log line gives them.
AS number or the whole service, or for a `reputation_hit` or a `waf_block`,
the client's own, as `client_group` gives it; both are empty for
`source_failure` and `file_error`. `asn`, `as_name` and `country` are, for a
ban, the client's as the ban's notes give them when the alert is raised:
empty, as in this alert, when GeoJS had not answered about the client by then;
for an `anomaly`, the client's as the lookup gave them by the time its request
ended; for a `reputation_hit` or a `waf_block`, the client's as its request's
log line gives them.
- `reason` is a short sentence; for a ban, the ban's `reason` in `bans.json`;
for an `anomaly`, what was counted over which threshold, such as
`requests per minute of the netblock 203.0.113.0/24 over the threshold of 1000`;
for a `reputation_hit`, `listed by a blocklist`,
`listed by the CrowdSec decision list`, `listed by a DNSBL zone` or
`scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_SCORE`.
`scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_SCORE`; for a `waf_block`,
`scored by the Core Rule Set at or over SWWAF_WAF_ANOMALY_THRESHOLD`.
- `detail` is what is particular to the event: for a ban, its `cause`, when it
ends as `ban_expires`, in the form the request log gives it, and its `notes`,
as `bans.json` gives them; for an `anomaly`, the `scope`, `client`, `net`,
@@ -1057,11 +1142,14 @@ is sent on one line:
or `hour`, the `kind`, `requests` or `bytes`, the `count`, which is weighted
as the rate limits weigh theirs, and the `threshold`; for a `reputation_hit`,
the `source`, the URL of the blocklist or of the CrowdSec decision list, the
zone or `abuseipdb`, and for AbuseIPDB the `score`; for `source_failure`, the
`source`, `geojs`, the URL of the list, the zone or `abuseipdb`, the `error`,
and for GeoJS, when it is asked again, `asking_again_in`; for `file_error`,
the `file`, which for an edit set aside is the file it was renamed to, and the
`error`, which for a file that does not parse names where in it the error is.
zone or `abuseipdb`, and for AbuseIPDB the `score`; for a `waf_block`, the
`rule_ids` and the `score`, as the request log gives them, the request's
`method`, its `path` with the query, and the `mode` when the request was not
refused for it; for `source_failure`, the `source`, `geojs`, the URL of the
list, the zone or `abuseipdb`, the `error`, and for GeoJS, when it is asked
again, `asking_again_in`; for `file_error`, the `file`, which for an edit set
aside is the file it was renamed to, and the `error`, which for a file that
does not parse names where in it the error is.
- `suppressed_repeats` is how many repeats the cooldown held back before this
alert, and for a `summary`, those no other alert gives (see below).
@@ -1182,9 +1270,10 @@ with times in UTC.
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.
`rule_blocked` for a request a `block` rule refused, `waf_blocked` for one the
Core Rule Set 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
@@ -1467,15 +1556,18 @@ scraped, and keeps this one as `exported_instance` unless the scrape sets
`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`, `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`,
`rule_blocked`, `waf_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
`smallwebwaf` runs; `smallwebwaf_active_bans` and
`smallwebwaf_permanent_bans`, neither of which counts a lifted ban.
- `smallwebwaf_rule_matches_total`: the requests that matched each rule, by
`rule_id` and `action`, the rule's own; and `smallwebwaf_rules_loaded`: the
rules read from the rule files.
- `smallwebwaf_waf_matches_total`: the requests that matched each rule of the
Core Rule Set, whatever their score, by `mode`, `SWWAF_WAF_MODE`, and
`rule_id`, a series for each rule that has matched.
- `smallwebwaf_country_requests_total`,
`smallwebwaf_country_request_bytes_total`,
`smallwebwaf_country_response_bytes_total`, and
@@ -1542,8 +1634,7 @@ scraped, and keeps this one as `exported_instance` unless the scrape sets
- Go's own `go_` metrics and the process's `process_` metrics.
The requests Go's HTTP server ends before `smallwebwaf` sees them (see "Request
log") are not counted. The metrics of the features still to come, such as the
Core Rule Set, come with them.
log") are not counted.
## Admin endpoints
@@ -2009,12 +2100,12 @@ addresses sends, so that one client costs at most one check every
Only a client whose history counts an offence is checked, so that the checks are
spent on suspects: 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. A client dropped
from the table of clients loses its history, and with it its offences. A client
is checked in the background, at its first request after its offence that
reaches the check: no request waits, a request refused under its ban is not
checked, and the request that has it checked, and any other from it before the
answer comes, goes on as from a client without a score.
matched a ban rule, or had a request refused by a block rule or the Core Rule
Set. A client dropped from the table of clients loses its history, and with it
its offences. A client is checked in the background, at its first request after
its offence that reaches the check: no request waits, a request refused under
its ban is not checked, and the request that has it checked, and any other from
it before the answer comes, goes on as from a client without a score.
A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE`, 75 by default, is a hit, and
`SWWAF_REPUTATION_ACTION` says what is done with its client, as for a DNSBL
@@ -2116,15 +2207,16 @@ alerts, the metrics nor `reputation.json` hold it. Given as a file, with
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 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.
for a `block` or `ban` rule, the latter banning the client, for a Core Rule
Set match in `block` mode, 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, a trap path or a Core Rule Set match 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
@@ -2132,6 +2224,9 @@ alerts, the metrics nor `reputation.json` hold it. Given as a file, with
and which ban `smallwebwaf` made is dropped when `SWWAF_MAX_BANS` are held.
- `internal/rules`: reads the rule files at start and again as they change, and
tells which of their rules a request matches.
- `internal/waf`: the Core Rule Set with the six changes, as Coraza's own
directives, and what it finds in a request's method, URL and headers: the
rules that matched and the anomaly score.
- `internal/lookup`: looks up each client's AS number and country through GeoJS,
keeps the answers, and hands each new one to the proxy, which adds it to the
client's history and to the notes of its bans; or in the lookup database,
@@ -2181,8 +2276,10 @@ the ban to drop past `SWWAF_MAX_BANS`, and `github.com/prometheus/client_golang`
keeps the metrics and serves them, and `github.com/fsnotify/fsnotify` tells
`smallwebwaf` when a state file or a rule file is saved, or the lookup database
replaced, and `github.com/oschwald/maxminddb-golang/v2` reads the lookup
database, which the tests write with `github.com/maxmind/mmdbwriter`. The
country codes are the list in `internal/config/config.go`.
database, which the tests write with `github.com/maxmind/mmdbwriter`, and
`github.com/corazawaf/coraza/v3` runs the Core Rule Set 4.25.0, which
`github.com/corazawaf/coraza-coreruleset/v4` at `v4.25.0` carries. The country
codes are the list in `internal/config/config.go`.
## Entrypoints