The Core Rule Set, run by Coraza, on each request's method, URL and headers (closes #25)
check / check (push) Waiting to run
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; SWWAF_WAF_DISABLED_RULES refuses 900000 to 900999, smallwebwaf's own rules among them. A request with more query parameters than Coraza reads, 1000, adds 5 (rule 900300). 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:
@@ -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,42 @@ 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 request with more than 1000 query
|
||||
parameters adds 5 (rule 900300), as a rule rated critical does, since Coraza
|
||||
reads only the first 1000. 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 +248,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 +311,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 +373,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 +664,37 @@ 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, as does one from 900000 to 900999, the ids of the
|
||||
rules that set the Core Rule Set up and of `smallwebwaf`'s own, so that no
|
||||
setting undoes its changes. 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 +702,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 +753,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 +801,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 +906,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 +944,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 +982,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 +1043,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 +1123,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 +1146,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 +1274,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 +1560,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 +1638,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 +2104,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 +2211,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 +2228,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 +2280,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user