SPEC: false statements fixed, repeats removed, dropped requirements restored
check / check (push) Waiting to run

The bounds sentence, the blocklist URL sentence, the deny list sentence,
the limit action's scope and the observe mode sentence now say what the
code does. Each of the twenty-one requirements the fourth review found
twice is stated once, with a link from the other place. The request id
header, redirect_uri's case, change 4's reason and the live archive's
InRelease files are back. The biased thresholds are defined where they
are introduced. The docs files apply the fifth README review's findings
and stay identical to the README branch.

Model: fable-5-1
This commit is contained in:
2026-10-22 22:35:29 +00:00
parent 4ea0a975c8
commit 23908b0e33
8 changed files with 303 additions and 283 deletions
+192 -192
View File
@@ -11,10 +11,10 @@ to them.
## Purpose ## Purpose
- It protects one self-hosted app on the open internet from scrapers, scanners - It protects one self-hosted app on the open internet from scrapers, scanners
and other abusers, from the app's first request, with no setting at all. Every and other abusers. It does so from the app's first request, with no setting at
setting has a default chosen for a service facing the internet in 2026. A all. Every setting has a default chosen for a service facing the internet
setting that needs an alert destination, an account key or a token stays off in 2026. A setting that needs an alert destination, an account key or a token
until the operator supplies it. stays off until the operator supplies it.
- It limits each client's requests and bytes per minute, hour and day. It bounds - It limits each client's requests and bytes per minute, hour and day. It bounds
the size and duration of every request and answer. It detects attacks. the size and duration of every request and answer. It detects attacks.
- It bans abusers: briefly at first, for seven days on a clear sign of attack, - It bans abusers: briefly at first, for seven days on a clear sign of attack,
@@ -60,8 +60,8 @@ to them.
- `internal/bans`: the bans. It holds each netblock's bans with their notes, - `internal/bans`: the bans. It holds each netblock's bans with their notes,
decides how long a new ban lasts, and picks the ban to drop when the table decides how long a new ban lasts, and picks the ban to drop when the table
of bans is full. of bans is full.
- `internal/rules`: reads the rule files, and again as they change, and - `internal/rules`: reads the rule files at start and again when they
tells which rules a request matches. change, and tells which rules a request matches.
- `internal/waf`: the Core Rule Set, with the six changes listed under - `internal/waf`: the Core Rule Set, with the six changes listed under
"Attack detection", as Coraza directives. "Attack detection", as Coraza directives.
- `internal/lookup`: looks clients up through GeoJS or in the lookup - `internal/lookup`: looks clients up through GeoJS or in the lookup
@@ -100,8 +100,8 @@ to them.
## What happens to a request ## What happens to a request
The steps run in this order. The first step that produces a final answer ends The steps run in this order. The first step that produces a final answer ends
the handling. The size and time limits apply to the whole exchange. Each step the checks; step 15 runs for every request. The size and time limits apply to
names the section that holds its rules. the whole exchange. Each step names the section that holds its rules.
1. `GET /_smallwebwaf/healthz` is answered at once, before any check. 1. `GET /_smallwebwaf/healthz` is answered at once, before any check.
2. The client is identified. 2. The client is identified.
@@ -147,20 +147,19 @@ names the section that holds its rules.
while `SWWAF_WAF_MODE` is `block`, and only logged and alerted while it is while `SWWAF_WAF_MODE` is `block`, and only logged and alerted while it is
`detect`. `detect`.
13. A request under `/_smallwebwaf/` is answered here (see "Admin endpoints"). 13. A request under `/_smallwebwaf/` is answered here (see "Admin endpoints").
14. The request is forwarded to `SWWAF_UPSTREAM_URL`, streaming, with 14. The request is forwarded to `SWWAF_UPSTREAM_URL`, streaming.
`X-Forwarded-For` added. With `SWWAF_ADD_LOOKUP_HEADERS`, `X-Client-ASN` and `X-Forwarded-For` is added, and `X-Request-ID` carries the request's id,
`X-Client-Country` are added for the app's own logs; a header whose value is generated when traefik did not supply one. With `SWWAF_ADD_LOOKUP_HEADERS`,
unknown is left out. Whatever that setting says, any such header the client `X-Client-ASN` and `X-Client-Country` are added for the app's own logs; a
sent is removed. header whose value is unknown is left out. Whatever that setting says, any
15. After the response: such header the client sent is removed.
- The response bytes, the request bytes or both are added to the byte 15. After the response, or after the refusal:
counters, as `SWWAF_BYTES_COUNT` says. For a WebSocket, what it carried - For a request forwarded to the app, the response bytes, the request bytes
each way counts once it closes. A client whose total passes a byte limit or both are added to the byte counters, as `SWWAF_BYTES_COUNT` says. For a
times the percentage has broken that limit and is banned. The response in WebSocket, what it carried each way counts once it closes. A client whose
progress is not cut off. total passes a byte limit times the percentage has broken that limit and
- A refusal by a rule file, a trap path, the Core Rule Set or a token check is banned. The response in progress is not cut off.
is counted for the error burst (see "Attack detection"). No other refusal - The refusals the error burst counts (see "Attack detection") are counted.
is.
- The client's history and the anomaly counters are updated, and the alert - The client's history and the anomaly counters are updated, and the alert
thresholds are evaluated. thresholds are evaluated.
@@ -183,7 +182,9 @@ the token check still apply.
with its history. The bans are bounded by `SWWAF_MAX_BANS` (see "Bans"). The with its history. The bans are bounded by `SWWAF_MAX_BANS` (see "Bans"). The
GeoJS answers, the reputation verdicts and scores, the DNS queries under way GeoJS answers, the reputation verdicts and scores, the DNS queries under way
and the anomaly counters have fixed bounds, listed under "Fixed limits" in and the anomaly counters have fixed bounds, listed under "Fixed limits" in
`docs/settings.md`; past a bound, the entry used longest ago goes first. `docs/settings.md`. Past its bound, the GeoJS answer or anomaly counter used
longest ago goes first, and the verdict or score fetched longest ago. Past
1,000 DNS queries under way, a zone is asked at the client's next request.
## Bans ## Bans
@@ -211,7 +212,8 @@ to a ban, each by its own rule:
- The first one bans the netblock for `SWWAF_ATTACK_BAN_DURATION` (default - The first one bans the netblock for `SWWAF_ATTACK_BAN_DURATION` (default
`7d`), with the cause `attack`. `7d`), with the cause `attack`.
- Any further request from the netblock while that ban lasts shows it is - Any further request from the netblock while that ban lasts shows it is
malicious: the ban becomes permanent. malicious: the ban becomes permanent. No other cause of ban becomes
permanent this way.
- Once that ban has run out, the netblock is served like any other. Its next - Once that ban has run out, the netblock is served like any other. Its next
clear sign of attack bans it permanently at once. clear sign of attack bans it permanently at once.
- A broken limit: a request over a rate limit, a response that takes the - A broken limit: a request over a rate limit, a response that takes the
@@ -238,8 +240,8 @@ A client on the CrowdSec decision list is banned until the decision ends, with
the cause `crowdsec`. Only a client that sends a request is banned, so a long the cause `crowdsec`. Only a client that sends a request is banned, so a long
list does not fill `bans.json`. Such a ban is never made permanent. It does not list does not fill `bans.json`. Such a ban is never made permanent. It does not
make the netblock's next ban for a broken limit longer. A ban lifted while the make the netblock's next ban for a broken limit longer. A ban lifted while the
decision is in force is followed by another at the client's next request; the decision is in force is made again at the client's next request. To lift it for
decision is deleted in CrowdSec first, and the ban lifted once the list has been good, delete the decision in CrowdSec, then lift the ban once the list has been
fetched again. fetched again.
These refuse each request they cover, record nothing in `bans.json`, and count These refuse each request they cover, record nothing in `bans.json`, and count
@@ -258,9 +260,9 @@ state") or through the ban endpoints (see "Admin endpoints"). A ban an admin
makes has the cause `admin`; an admin who wants to keep a ban `smallwebwaf` made makes has the cause `admin`; an admin who wants to keep a ban `smallwebwaf` made
sets its cause to `admin`. A ban lifted before it ends is kept, marked as sets its cause to `admin`. A ban lifted before it ends is kept, marked as
lifted, and does not count toward a longer ban; it refuses nothing. Deleting its lifted, and does not count toward a longer ban; it refuses nothing. Deleting its
entry from `bans.json` forgets it entirely. Only a ban whose cause is `attack` entry from `bans.json` forgets it entirely. An admin whose own address is banned
becomes permanent at the first request it refuses. An admin whose own address is lifts the ban by editing `bans.json`, since the endpoints refuse a banned
banned lifts the ban by editing `bans.json`. client's request like any other.
A permanent ban does not run out. It ends when an admin lifts it, or when A permanent ban does not run out. It ends when an admin lifts it, or when
`smallwebwaf` drops it from a full table, as below. When `smallwebwaf` holds `smallwebwaf` drops it from a full table, as below. When `smallwebwaf` holds
@@ -274,8 +276,7 @@ table.
## Settings ## Settings
`docs/settings.md` lists every setting with its default and the values it takes, `docs/settings.md` lists every setting with its default and the values it takes,
in the groups below. It also gives the forms of values: lists, netblocks, in the groups below. It also gives the form each kind of value takes.
durations, sizes, countries, AS numbers, percentages and path prefixes.
Conventions: Conventions:
- These settings take `off`, which switches them off: - These settings take `off`, which switches them off:
@@ -301,21 +302,18 @@ Conventions:
restarting the container. The files `smallwebwaf` watches while it runs are restarting the container. The files `smallwebwaf` watches while it runs are
its state files, its rule files and the lookup database. its state files, its rule files and the lookup database.
- An invalid setting stops the process at start with one log line, marked - An invalid setting stops the process at start with one log line, marked
`"type":"process"` with `"msg":"invalid setting"`, naming the variable and `"type":"process"` with `"msg":"invalid setting"`. The line names the variable
what is wrong with the value. runit then starts the process again every second and what is wrong with the value; "The two processes" says what happens then.
until the value is fixed (see "The two processes"). At start the settings in At start the settings in effect are logged on one line; tokens, keys and the
effect are logged on one line; tokens, keys and the paths of webhooks, which paths of webhooks, which often carry a secret, are shown as `********`.
often carry a secret, are shown as `********`.
### Core ### Core
- One listener, `SWWAF_LISTEN_ADDR`, for the requests `smallwebwaf` forwards to
`SWWAF_UPSTREAM_URL` and for its own endpoints (see "Ports").
- `SWWAF_INSTANCE_NAME` is in every log line, metric and alert, so that - `SWWAF_INSTANCE_NAME` is in every log line, metric and alert, so that
instances can be told apart. The default, the container's host name, changes instances can be told apart. The default, the container's host name, changes
at every redeploy; a name set by the operator survives it. at every redeploy; a name set by the operator survives it.
- `SWWAF_MODE=observe` logs and alerts on every decision while refusing nothing, - `SWWAF_MODE=observe` lets a configuration be tried before it is enforced (see
so that a configuration can be tried before it is enforced. "What happens to a request").
- `X-Forwarded-For` is believed only from `SWWAF_TRUSTED_PROXIES`, by default - `X-Forwarded-For` is believed only from `SWWAF_TRUSTED_PROXIES`, by default
the private address ranges, so a client that connects directly cannot claim the private address ranges, so a client that connects directly cannot claim
another address. Where other containers reach `smallwebwaf` directly, or another address. Where other containers reach `smallwebwaf` directly, or
@@ -350,14 +348,12 @@ Conventions:
### Logging ### Logging
- The request log on stdout is always on and has no switch (see "Request log"). - The request log on stdout is always on and has no switch (see "Request log").
The process's own messages share the stream; `SWWAF_LOG_LEVEL` filters only `SWWAF_LOG_LEVEL` filters only the process's own messages, and no message is
those, and no message is at `debug`. `warn` and `error` hold back the line of at `debug`. `warn` and `error` hold back the line of settings at start.
settings at start.
- `SWWAF_LOG_REQUEST_HEADERS` names the request headers to record. - `SWWAF_LOG_REQUEST_HEADERS` names the request headers to record.
`Authorization`, `Cookie` and `Set-Cookie` values are never logged, even if `Authorization`, `Cookie` and `Set-Cookie` values are never logged, even if
listed, only whether they were present. listed, only whether they were present.
- `SWWAF_LOG_REMOTE_URL` sends every log line to a syslog server as well, over - `SWWAF_LOG_REMOTE_URL` names a syslog server (see "Request log").
UDP, TCP or TLS (see "Request log").
### Metrics ### Metrics
@@ -370,7 +366,6 @@ Conventions:
- `SWWAF_ALLOW_NETS` skips the checks step 3 names, for monitoring or the - `SWWAF_ALLOW_NETS` skips the checks step 3 names, for monitoring or the
owner's own networks. `SWWAF_RATE_LIMIT_EXEMPT_NETS` bypasses the request and owner's own networks. `SWWAF_RATE_LIMIT_EXEMPT_NETS` bypasses the request and
byte limits only; the error burst, attack detection and bans still apply. byte limits only; the error burst, attack detection and bans still apply.
`SWWAF_DENY_NETS` is always refused.
- Many clients share one address (mobile carriers, offices, Tor), and so share - Many clients share one address (mobile carriers, offices, Tor), and so share
its limits and its bans. The defaults sit well above one person's use, and the its limits and its bans. The defaults sit well above one person's use, and the
first two lists take known shared addresses out. first two lists take known shared addresses out.
@@ -379,9 +374,8 @@ Conventions:
Breaking one bans the client, so the defaults sit several times above what one Breaking one bans the client, so the defaults sit several times above what one
busy person produces. A browser loading a heavy page makes a few hundred busy person produces. A browser loading a heavy page makes a few hundred
requests, a git clone a handful, and several people often share one address. A requests, and a git clone a handful. A machine that talks to the app all day can
machine that talks to the app all day can still pass the day limit. Its address still pass the day limit. Its address belongs in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
belongs in `SWWAF_RATE_LIMIT_EXEMPT_NETS`.
`SWWAF_RATE_LIMIT_EXEMPT_PATHS` takes path prefixes out of the count, such as `SWWAF_RATE_LIMIT_EXEMPT_PATHS` takes path prefixes out of the count, such as
static assets or health checks; their bytes are not counted either. A prefix is static assets or health checks; their bytes are not counted either. A prefix is
@@ -394,10 +388,9 @@ matched the same way.
### Byte limits, per client ### Byte limits, per client
A response's bytes are counted when it ends, so a byte limit never cuts an A response's bytes are counted when it ends (see step 15). So every default sits
answer short; the client's next request is refused instead. Every default sits above the largest request and answer together, and no single download breaks a
above the largest request and answer together, so at the defaults no single limit.
download breaks a limit.
### Size and time limits ### Size and time limits
@@ -476,10 +469,9 @@ service that keeps its visitors' addresses on its own host.
request to GeoJS. A client whose answer has not come in time counts as unknown request to GeoJS. A client whose answer has not come in time counts as unknown
until it comes, and its later requests do not wait. Its address is asked about until it comes, and its later requests do not wait. Its address is asked about
again in the background. again in the background.
- A client the lookup cannot place has an unknown country and AS number. The - A client the lookup cannot place has an unknown country and AS number, which
biased thresholds give it `SWWAF_UNKNOWN_LIMIT_PERCENT`, and the exclusive the country lists and the biased thresholds treat as described there. Nothing
country list refuses it (see "Country lists"). Nothing else treats it else treats it differently.
differently.
- GeoJS publishes no rate limit, but its terms forbid "an excessive amount of - GeoJS publishes no rate limit, but its terms forbid "an excessive amount of
API requests", judged by GeoJS alone, and let it block a caller. While GeoJS API requests", judged by GeoJS alone, and let it block a caller. While GeoJS
is slow, down or refusing `smallwebwaf`, new clients count as unknown. GeoJS is slow, down or refusing `smallwebwaf`, new clients count as unknown. GeoJS
@@ -489,21 +481,19 @@ service that keeps its visitors' addresses on its own host.
- The file is the free IPinfo Lite database (`ipinfo_lite.mmdb`), whose `asn`, - The file is the free IPinfo Lite database (`ipinfo_lite.mmdb`), whose `asn`,
`as_name` and `country_code` fields are read. The operator downloads it with a `as_name` and `country_code` fields are read. The operator downloads it with a
free IPinfo account and mounts it read-only; `smallwebwaf` never fetches it free IPinfo account and mounts it read-only; `smallwebwaf` never fetches it
and holds no account or token for it. IPinfo updates it daily, and releases it and holds no account or token for it. IPinfo updates it daily. Its licence is
under the Creative Commons Attribution-ShareAlike 4.0 licence, asking for a Creative Commons Attribution-ShareAlike 4.0, and IPinfo asks for a link to it
link to IPinfo on the site that uses it, such as "IP address data is powered on the site that uses it, such as "IP address data is powered by IPinfo".
by IPinfo".
- The file is read whole at start; one that is missing or unreadable stops the - The file is read whole at start; one that is missing or unreadable stops the
start. It is read again once it has gone 2 seconds without a change after start. It is read again once it has gone 2 seconds without a change after
being replaced. One that cannot be read is logged, sent as one `file_error` being replaced. One that cannot be read is logged, sent as one `file_error`
alert and ignored. The database already loaded stays in use. The directory alert and ignored. The database already loaded stays in use. A client whose
that holds the file is mounted, not the file itself (see "Rule files" for address the file does not hold counts as unknown.
why). A client whose address the file does not hold counts as unknown.
### Country lists ### Country lists
Both are empty by default: the defaults judge a client by what it does, not by Both are empty by default: the defaults judge a client by what it does, not by
where it comes from. Clients in `SWWAF_ALLOW_NETS` are not checked. where it comes from.
- `SWWAF_DENIED_COUNTRIES` refuses every request from a listed country. - `SWWAF_DENIED_COUNTRIES` refuses every request from a listed country.
- `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` lets through only clients placed in a - `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` lets through only clients placed in a
@@ -524,9 +514,10 @@ where it comes from. Clients in `SWWAF_ALLOW_NETS` are not checked.
### Biased thresholds ### Biased thresholds
The lists are empty by default, for the same reason as the country lists. The The biased thresholds lower the limits of clients in listed AS numbers and
request log and the metrics show which AS numbers and countries a service's countries. The lists are empty by default, for the same reason as the country
abuse comes from. lists. The request log and the metrics show which AS numbers and countries a
service's abuse comes from.
- Clients in a listed AS number or country get that percentage of every request - Clients in a listed AS number or country get that percentage of every request
and byte limit (`SWWAF_ASN_LIMIT_PERCENT`, `SWWAF_COUNTRY_LIMIT_PERCENT`). A and byte limit (`SWWAF_ASN_LIMIT_PERCENT`, `SWWAF_COUNTRY_LIMIT_PERCENT`). A
@@ -566,9 +557,8 @@ body. The rule files, the limits and the bans still apply to the client that
sends it. sends it.
- The Core Rule Set inspects only a request that every earlier check let - The Core Rule Set inspects only a request that every earlier check let
through. It skips a path under `SWWAF_WAF_EXEMPT_PATHS` and a client in through, and skips a path under `SWWAF_WAF_EXEMPT_PATHS`. An engine error on a
`SWWAF_ALLOW_NETS`. An engine error on a request forwards the request, and is request forwards the request, and is logged and counted.
logged and counted.
- `SWWAF_WAF_PARANOIA_LEVEL` and `SWWAF_WAF_ANOMALY_THRESHOLD` are the Core Rule - `SWWAF_WAF_PARANOIA_LEVEL` and `SWWAF_WAF_ANOMALY_THRESHOLD` are the Core Rule
Set's own two tuning values, at its own defaults. Each level up runs more Set's own two tuning values, at its own defaults. Each level up runs more
rules, which find more attacks and take more ordinary requests for attacks. rules, which find more attacks and take more ordinary requests for attacks.
@@ -577,8 +567,8 @@ sends it.
default one critical rule is enough. With the threshold `off`, nothing is a default one critical rule is enough. With the threshold `off`, nothing is a
match, and scores are still logged. A request with more than 1000 query match, and scores are still logged. A request with more than 1000 query
parameters or body fields adds 5 to its score, since Coraza reads only the parameters or body fields adds 5 to its score, since Coraza reads only the
first 1000. Either way the log line gives the rule ids and the score, and a first 1000. For a match, in `block` and `detect` mode alike, the log line
`waf_block` alert is sent. gives the rule ids and the score, and a `waf_block` alert is sent.
- `SWWAF_WAF_DISABLED_RULES` switches off rule ids when an app trips a false - `SWWAF_WAF_DISABLED_RULES` switches off rule ids when an app trips a false
positive. The default switches off six rules: positive. The default switches off six rules:
- 920340, 920420 and 920640 refuse a request for the type of its body, when - 920340, 920420 and 920640 refuse a request for the type of its body, when
@@ -605,19 +595,20 @@ sends it.
to send the part that is read. Body inspection suits apps whose forms carry no to send the part that is read. Body inspection suits apps whose forms carry no
code; `docs/gitea.md` records what it refuses in front of gitea. code; `docs/gitea.md` records what it refuses in front of gitea.
- `SWWAF_TRAP_PATHS` names paths the app never serves and only scanners ask for, - `SWWAF_TRAP_PATHS` names paths the app never serves and only scanners ask for,
such as `/wp-login.php` in front of gitea. A request for one is a clear sign such as `/wp-login.php` in front of gitea. This is the short form of a `path`
of attack. This is the short form of a `path` rule with the `ban` action, for rule with the `ban` action, for deployments that mount no rule files. A trap
deployments that mount no rule files. A trap path is matched whole, without path is matched whole, without the query, against the path as sent and once
the query, against the path as sent and once percent-decoded: `/wp-login.php` percent-decoded: `/wp-login.php` matches `/wp-login.php?x=1` and
matches `/wp-login.php?x=1` and `/%77p-login.php`, and does not match `/%77p-login.php`, and does not match `/wp-login.php/` or
`/wp-login.php/` or `/blog/wp-login.php`. `/blog/wp-login.php`.
- `SWWAF_ERROR_BURST_THRESHOLD` counts a client's refusals per minute for a rule - `SWWAF_ERROR_BURST_THRESHOLD` counts a client's refusals per minute for a rule
file match, a trap path, a Core Rule Set match, or a missing or wrong token. file match, a trap path, a Core Rule Set match, or a missing or wrong token;
More than this breaks a limit (see "Bans"). A client trying one attack or no other refusal counts. More than this breaks a limit (see "Bans"). A client
token after another is refused many times a minute; a person rarely more than trying one attack or token after another is refused many times a minute; a
a few times. Answers the app gives are not counted, since they do not tell a person rarely more than a few times. Answers the app gives are not counted,
scanner from an ordinary client. In front of gitea, registry and git clients since they do not tell a scanner from an ordinary client. In front of gitea,
get `404` and `401` by design, hundreds of times a minute. registry and git clients get `404` and `401` by design, hundreds of times a
minute.
`smallwebwaf` changes the Core Rule Set 4.25.0 in six ways, since in front of `smallwebwaf` changes the Core Rule Set 4.25.0 in six ways, since in front of
gitea it would otherwise refuse ordinary requests. The changes hold in front of gitea it would otherwise refuse ordinary requests. The changes hold in front of
@@ -645,15 +636,15 @@ every app, and no setting undoes them:
- `artifactName` and `redirect_to`. - `artifactName` and `redirect_to`.
gitea puts file paths, branch and workflow names, and the page to return to gitea puts file paths, branch and workflow names, and the page to return to
after signing in into them. Any name on those lists can be an ordinary file after signing in into them. It reads these values as names in a repository
or branch, such as `.gitignore` or `docker-build`. In these parameters, or in its own records, or as a page of its own site, never as a command. Any
anything only these three rules catch reaches the app, commands such as name on those lists can be an ordinary file or branch, such as `.gitignore`
`|cat /etc/passwd` included. Path traversal, SQL and script injection, and or `docker-build`. In these parameters, anything only these three rules
PHP, Java and Node.js code are still refused there. Every other parameter catch reaches the app, commands such as `|cat /etc/passwd` included. Path
keeps all three rules. Parameter names are matched without regard to case, traversal, SQL and script injection, and PHP, Java and Node.js code are
as Coraza matches them. An app that uses one of these parameters as a file still refused there. Every other parameter keeps all three rules. An app
on the server, or passes it to a shell, must check the value itself. A rule that uses one of these parameters as a file on the server, or passes it to a
file can refuse it instead. shell, must check the value itself. A rule file can refuse it instead.
5. Two cookies are left out, and `Referer` keeps fewer rules: 5. Two cookies are left out, and `Referer` keeps fewer rules:
- The Core Rule Set reads the request without the `gitea_flash` and - The Core Rule Set reads the request without the `gitea_flash` and
@@ -677,6 +668,9 @@ every app, and no setting undoes them:
6. Responses are not inspected. A raw file from a repository, such as a shell 6. Responses are not inspected. A raw file from a repository, such as a shell
script, looks to the response rules like source code leaking from the server. script, looks to the response rules like source code leaking from the server.
The parameter and cookie names in these changes are matched without regard to
case, as Coraza matches them.
### Ban lengths, response and scope ### Ban lengths, response and scope
`SWWAF_BAN_RESPONSE` is `403` by default, which makes a ban easy to recognise `SWWAF_BAN_RESPONSE` is `403` by default, which makes a ban easy to recognise
@@ -690,10 +684,10 @@ to the surrounding netblock. The four ban lengths are under "Bans".
No source is on by default. A source acts on a list kept elsewhere; the defaults No source is on by default. A source acts on a list kept elsewhere; the defaults
judge a client only by what it does here. AbuseIPDB and CrowdSec also need an judge a client only by what it does here. AbuseIPDB and CrowdSec also need an
account or an engine of the operator's own. The list best suited to be on by account or an engine of the operator's own. The list best suited to be on by
default, Spamhaus DROP, must not be fetched more than once an hour, and Spamhaus default, Spamhaus DROP, has terms, below, that forbid frequent fetching, and
may block an address that does. Each `smallwebwaf` fetches its own copy, so Spamhaus may block an address that breaks them. Each `smallwebwaf` fetches its
several apps on one host, sharing one address, would often fetch less than an own copy, so several apps on one host, sharing one address, would often fetch
hour apart. too close together.
- Blocklists (`SWWAF_BLOCKLIST_URLS`) are text files of addresses and netblocks, - Blocklists (`SWWAF_BLOCKLIST_URLS`) are text files of addresses and netblocks,
one to a line, as the Spamhaus DROP list is written. Anything after a `;` or one to a line, as the Spamhaus DROP list is written. Anything after a `;` or
@@ -718,33 +712,34 @@ hour apart.
`AS<number>:<percent>`. `AS<number>:<percent>`.
The log, the alerts, the ban notes and the metrics name each list by its The log, the alerts, the ban notes and the metrics name each list by its
URL, so a secret stays out of it. URL, so a list's URL must not hold a secret.
- `SWWAF_BLOCKLIST_ACTION` is `deny` by default, which refuses every request - `SWWAF_BLOCKLIST_ACTION` is `deny` by default, which refuses every request
from a listed address and suits lists of networks that send nothing from a listed address and suits lists of networks that send nothing
legitimate, such as DROP. `limit:<percent>` gives listed clients that legitimate, such as DROP. `limit:<percent>` gives listed clients that
percentage of every limit, and suits a list of addresses shared with ordinary percentage of every request and byte limit, and suits a list of addresses
visitors, such as Tor exits. `log` only notes the hit in the log line and shared with ordinary visitors, such as Tor exits. `log` only notes the hit in
raises an alert. the log line and raises an alert.
- A CrowdSec engine on the host (`SWWAF_CROWDSEC_LAPI_URL`) has its decision - A CrowdSec engine on the host (`SWWAF_CROWDSEC_LAPI_URL`) has its decision
list fetched every minute, with `SWWAF_CROWDSEC_LAPI_KEY` in the `X-Api-Key` list fetched every minute, with `SWWAF_CROWDSEC_LAPI_KEY` in the `X-Api-Key`
header. The list is kept like a fetched blocklist, and a client on it is header. The list is kept like a fetched blocklist, and a client on it is
banned (see "Bans"). Only decisions of the type `ban` on an address or a banned (see "Bans"). Only decisions of the type `ban` on an address or a
netblock are used; any other, such as a captcha or a country, is left out. A netblock are used; any other, such as a captcha or a country, is left out. A
decision lasts for the `duration` it had left when the engine answered. A decision lasts for the `duration` it had left when the engine answered. This
fetch fails on an answer other than `200`, a redirect included, on one not is how a fleet-wide blocklist can arrive without `smallwebwaf` depending on
finished within a minute, on one longer than 16 MiB, and on one that is not a CrowdSec. A fetch fails when:
list of decisions. This is how a fleet-wide blocklist can arrive without - the engine answers other than `200`, a redirect included;
`smallwebwaf` depending on CrowdSec. - the fetch does not finish within a minute;
- the answer is longer than 16 MiB;
- the answer is not a list of decisions.
- Each DNS blocklist (`SWWAF_DNSBL_ZONES`) is asked about a client's own address - Each DNS blocklist (`SWWAF_DNSBL_ZONES`) is asked about a client's own address
in the background, the first time the client is seen; the request goes on at in the background, the first time the client is seen. No request waits for the
once, and so does every request from the client until the zone has answered. answer. The name asked about is the one RFC 5782 gives: the numbers of an IPv4
The name asked about is the one RFC 5782 gives: the numbers of an IPv4 address address reversed, or the 32 hex digits of an IPv6 address reversed. So
reversed, or the 32 hex digits of an IPv6 address reversed, so `192.0.2.99` is `192.0.2.99` is asked about as `99.2.0.192.dnsbl.dronebl.org`. An answer in
asked about as `99.2.0.192.dnsbl.dronebl.org`. An answer in `127.0.0.0/8` `127.0.0.0/8` lists the client, but one in `127.255.255.0/24` is Spamhaus
lists the client, but one in `127.255.255.0/24` is Spamhaus refusing the refusing the query. A name that does not exist does not list the client. These
query. A name that does not exist does not list the client. These are are failures, after which the zone is not asked again for a minute:
failures, after which the zone is not asked again for a minute:
- an answer in `127.255.255.0/24`; - an answer in `127.255.255.0/24`;
- an answer outside `127.0.0.0/8`; - an answer outside `127.0.0.0/8`;
- a resolver error; - a resolver error;
@@ -752,10 +747,10 @@ hour apart.
DNS blocklists made for mail servers list residential ranges, and would DNS blocklists made for mail servers list residential ranges, and would
block ordinary visitors, so they should not be used. Spamhaus zones need block ordinary visitors, so they should not be used. Spamhaus zones need
their keyed query service, given as a zone name containing the key, which their keyed query service, given as a zone name that contains the key.
`smallwebwaf` shows as `********` wherever it names the zone; only `smallwebwaf` shows the key as `********` wherever it names the zone. Only
`reputation.json` keeps the zone with its key. `SWWAF_DNSBL_RESOLVER` names `reputation.json` keeps the key. `SWWAF_DNSBL_RESOLVER` names a resolver,
a resolver, since public resolvers are refused by several list operators. since public resolvers are refused by several list operators.
- AbuseIPDB (`SWWAF_ABUSEIPDB_KEY`) is asked, at its check endpoint, for the - AbuseIPDB (`SWWAF_ABUSEIPDB_KEY`) is asked, at its check endpoint, for the
abuse confidence score of a client, from 0 to 100. Only a client that has abuse confidence score of a client, from 0 to 100. Only a client that has
@@ -767,14 +762,16 @@ hour apart.
address of the request that set off the check, and the score is used for the address of the request that set off the check, and the score is used for the
whole group. A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE` is a hit. The whole group. A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE` is a hit. The
budget is counted in UTC days and kept in `reputation.json`, so a restart does budget is counted in UTC days and kept in `reputation.json`, so a restart does
not make it whole again; the check that spends the last of it is logged and not make it whole again. The check that spends the last of it is logged and
alerted, and no client is checked until the day ends. A check fails on an alerted, and no client is checked until the day ends. The key is sent in the
answer other than `200`, on one without a score, and on no answer within `Key` header and nowhere else, and is never logged. A check fails, after which
`SWWAF_REPUTATION_TIMEOUT`; after a failure no client is checked for a minute. no client is checked for a minute, when:
The key is sent in the `Key` header and nowhere else, and is never logged. - AbuseIPDB answers other than `200`;
- the answer gives no score;
- no answer comes within `SWWAF_REPUTATION_TIMEOUT`.
- `SWWAF_REPUTATION_ACTION`, for DNS blocklist and AbuseIPDB hits, is `limit:25` - `SWWAF_REPUTATION_ACTION`, for DNS blocklist and AbuseIPDB hits, is `limit:25`
by default. Such verdicts are less certain than a blocklist, so a listed by default. Such verdicts are less certain than a blocklist, so a listed
client gets a quarter of every limit rather than a refusal. client gets a quarter of every request and byte limit rather than a refusal.
- Verdicts and scores are kept for `SWWAF_REPUTATION_CACHE_TTL`, in memory and - Verdicts and scores are kept for `SWWAF_REPUTATION_CACHE_TTL`, in memory and
in `reputation.json`, with the fixed bounds under "Fixed limits" in in `reputation.json`, with the fixed bounds under "Fixed limits" in
`docs/settings.md`. A source that is down or over quota gives no verdict. The `docs/settings.md`. A source that is down or over quota gives no verdict. The
@@ -797,13 +794,13 @@ hour apart.
counted by event, so a wide attack cannot flood the channel. Once the hour counted by event, so a wide attack cannot flood the channel. Once the hour
ends, one alert with the event `summary` gives those counts. It also counts ends, one alert with the event `summary` gives those counts. It also counts
the repeats that a cooldown held back and that no later alert reported. the repeats that a cooldown held back and that no later alert reported.
- Each destination has a queue of at most 1000 alerts, sent one at a time, so a - Each destination has a bounded queue (see "Fixed limits" in
slow destination holds up nothing; when it is full, the oldest alert is `docs/settings.md`), sent one at a time, so a slow destination holds up
dropped, and the drops are counted in the metrics. A `2xx` answer takes an nothing. When a queue is full, its oldest alert is dropped and counted in the
alert. A `4xx` other than `408` and `429` refuses it, and it is dropped. metrics. A `2xx` answer takes an alert. A `4xx` other than `408` and `429`
Anything else is a failure: the alert is sent again a second later, and twice refuses it, and it is dropped. Anything else is a failure: the alert is sent
as long after each further failure, up to a minute. `alerts.json` keeps the again a second later, and twice as long after each further failure, up to a
queues and cooldowns across a restart. minute. `alerts.json` keeps the queues and cooldowns across a restart.
### Anomaly thresholds ### Anomaly thresholds
@@ -814,9 +811,12 @@ which the metrics show. They reveal a widely distributed scraper, thousands of
addresses at low rates each, which the per-client limits do not see. A low addresses at low rates each, which the per-client limits do not see. A low
`SWWAF_ASN_LIMIT_PERCENT` for its AS number is the response. `SWWAF_ASN_LIMIT_PERCENT` for its AS number is the response.
- Requests and bytes are counted per minute and per hour, for each client, each - Requests and bytes are counted per minute and per hour, for each of:
surrounding netblock, each AS number, the whole service, and each named - a client;
netblock in `SWWAF_WATCH_NETS`. - the surrounding netblock;
- an AS number;
- the whole service;
- a named netblock in `SWWAF_WATCH_NETS`.
- Every request but the health check is counted, whatever was done with it, - Every request but the health check is counted, whatever was done with it,
clients in `SWWAF_ALLOW_NETS` and `SWWAF_RATE_LIMIT_EXEMPT_NETS` included, clients in `SWWAF_ALLOW_NETS` and `SWWAF_RATE_LIMIT_EXEMPT_NETS` included,
since an exempt client misbehaving is worth knowing about. The counters are since an exempt client misbehaving is worth knowing about. The counters are
@@ -825,8 +825,8 @@ addresses at low rates each, which the per-client limits do not see. A low
## Rule files ## Rule files
`smallwebwaf` reads every `*.rules` file in `SWWAF_RULES_DIR` and checks each `smallwebwaf` reads every `*.rules` file in `SWWAF_RULES_DIR` and checks each
request against them, unless the client is in `SWWAF_ALLOW_NETS`. request against them. `docs/rules.md` gives the format, the targets and actions,
`docs/rules.md` gives the format, the targets and actions, and the default file. and the default file.
- The files are plain text meant to be edited by hand, so that a new scanning - The files are plain text meant to be edited by hand, so that a new scanning
pattern seen in the request log becomes a rule in one line. `smallwebwaf` pattern seen in the request log becomes a rule in one line. `smallwebwaf`
@@ -835,7 +835,7 @@ request against them, unless the client is in `SWWAF_ALLOW_NETS`.
for 2 seconds, so that a file still being written is read whole. A file whose for 2 seconds, so that a file still being written is read whole. A file whose
name starts with `.`, such as an editor's lock file, is not a rule file. name starts with `.`, such as an editor's lock file, is not a rule file.
- Files are read in name order (`00-default.rules` before `50-gitea.rules`), - Files are read in name order (`00-default.rules` before `50-gitea.rules`),
rules in line order. Matching stops at the first rule that refuses. rules in line order.
- A rule's regex is Go's RE2 syntax. It has no backreferences or lookaround; in - A rule's regex is Go's RE2 syntax. It has no backreferences or lookaround; in
exchange matching time is linear in the input, so no rule can be made to stall exchange matching time is linear in the input, so no rule can be made to stall
the proxy. the proxy.
@@ -843,18 +843,16 @@ request against them, unless the client is in `SWWAF_ALLOW_NETS`.
encoded probe cannot slip past. A header sent more than once is matched with encoded probe cannot slip past. A header sent more than once is matched with
its values joined by `, `; a header not sent is empty text. `Host` and its values joined by `, `; a header not sent is empty text. `Host` and
`Transfer-Encoding` cannot be targets, since Go's HTTP server takes them out `Transfer-Encoding` cannot be targets, since Go's HTTP server takes them out
of every request. Request bodies are not available to rule files; reading them of every request; a rule naming one stops the start like any bad line. Request
is the Core Rule Set's job. bodies are not available to rule files; reading them is the Core Rule Set's
job.
- `log` only notes the match in the request log. `block` refuses the request - `log` only notes the match in the request log. `block` refuses the request
without a ban; `ban` makes the request a clear sign of attack (see "Bans"). without a ban; `ban` makes the request a clear sign of attack (see "Bans"),
`ban` is for requests no real visitor sends, anchored at the site root. A file and is for requests no real visitor sends. A visitor banned by mistake is let
of the same name deeper in a site can be ordinary content, such as a file in a back in by lifting the ban.
gitea repository. A visitor banned by mistake is let back in by lifting the
ban.
- At start, these stop the process with a message naming the file and line: - At start, these stop the process with a message naming the file and line:
- a line that does not parse; - a line that does not parse;
- a regex that does not compile; - a regex that does not compile;
- a rule for `Host` or `Transfer-Encoding`;
- a duplicate id. - a duplicate id.
So does a `SWWAF_RULES_DIR` that does not exist or cannot be read. While So does a `SWWAF_RULES_DIR` that does not exist or cannot be read. While
@@ -867,10 +865,11 @@ request against them, unless the client is in `SWWAF_ALLOW_NETS`.
wrong for every app, with its path rules anchored at the site root. In front wrong for every app, with its path rules anchored at the site root. In front
of gitea, `/.env` is a probe, while of gitea, `/.env` is a probe, while
`/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository that `/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository that
any visitor may open. Anything app-specific, such as a rule banning WordPress any visitor may open. `ban` rules of your own follow the same rule. Anything
probes in front of gitea, belongs in a file the app's Dockerfile adds or the app-specific, such as a rule banning WordPress probes in front of gitea,
deployer mounts. A directory mounted over `SWWAF_RULES_DIR` replaces the belongs in a file the app's Dockerfile adds or the deployer mounts. A
default file; single files mounted into it add to it. directory mounted over `SWWAF_RULES_DIR` replaces the default file; single
files mounted into it add to it.
- Rules meant to be edited while `smallwebwaf` runs belong in a mounted - Rules meant to be edited while `smallwebwaf` runs belong in a mounted
directory, not a mounted file. Many editors save a file by replacing it, and a directory, not a mounted file. Many editors save a file by replacing it, and a
container keeps seeing the old one when the file alone is mounted. The lookup container keeps seeing the old one when the file alone is mounted. The lookup
@@ -880,11 +879,10 @@ request against them, unless the client is in `SWWAF_ALLOW_NETS`.
## Persistent state ## Persistent state
All state lives in memory, and the files in `SWWAF_STATE_DIR` hold a copy of all All state lives in memory, and the files in `SWWAF_STATE_DIR` hold a copy of all
of it, so an orderly stop loses nothing. The one exception is log lines still of it, so an orderly stop loses nothing (the remote log is the exception; see
waiting to be sent to `SWWAF_LOG_REMOTE_URL`, which stdout has already carried. "Request log"). The files are meant for people as well: an admin can read or
The files are meant for people as well: an admin can read or edit them at any edit them at any time, and the running `smallwebwaf` takes the edit in.
time, and the running `smallwebwaf` takes the edit in. `docs/state-files.md` `docs/state-files.md` lists the files, what each holds, and their sizes.
lists the files, what each holds, and their sizes.
- Each file holds one kind of state, as JSON with: - Each file holds one kind of state, as JSON with:
- a top-level `version` number; - a top-level `version` number;
@@ -897,9 +895,9 @@ lists the files, what each holds, and their sizes.
halves their size and lets `grep` show everything about one client. halves their size and lets `grep` show everything about one client.
- Clients that were never banned are kept too, so every client's history - Clients that were never banned are kept too, so every client's history
survives a restart. survives a restart.
- Each file has a bound, so it can be rewritten whole. Writing a file of these - Each file has a bound, so it can be rewritten whole. Writing one takes well
sizes takes well under a second, in the background, so rewriting whole files under a second, in the background, so rewriting whole files needs nothing
needs nothing cleverer. cleverer.
- A write serialises from a snapshot taken under the lock, so requests are not - A write serialises from a snapshot taken under the lock, so requests are not
held up. It goes to a temporary file in the same directory, which is synced held up. It goes to a temporary file in the same directory, which is synced
and renamed over the real name, and the directory is synced. A crash at any and renamed over the real name, and the directory is synced. A crash at any
@@ -907,23 +905,26 @@ lists the files, what each holds, and their sizes.
partial one. Before writing a file, `smallwebwaf` takes in any edit made to it partial one. Before writing a file, `smallwebwaf` takes in any edit made to it
on disk, so an admin's edit is never overwritten. on disk, so an admin's edit is never overwritten.
- At start, the files are read back. Active bans go into an in-memory lookup by - At start, the files are read back. Active bans go into an in-memory lookup by
netblock, everything else into maps. Counter buckets and cache entries whose netblock, everything else into maps. Expired cache entries are discarded. A
time has passed are discarded. A missing file means empty state, and is normal missing file means empty state, and is normal on first run. These stop the
on first run. A file that does not parse, or has an unknown `version`, stops process with a message naming the file and position:
the process with a message naming the file and position; so does an entry - a file that does not parse, or has an unknown `version`;
without a field it needs, a ban whose cause is not one of the four, or a ban - an entry without a field it needs;
whose `expires` is not after its `start`. Starting with empty state would - a ban whose cause is not one of the four;
silently forgive every repeat offender. Since writes are atomic, a broken file - a ban whose `expires` is not after its `start`.
can only come from a hand edit, which the editor should hear about.
Starting with empty state would silently forgive every repeat offender.
Since writes are atomic, a broken file can only come from a hand edit, which
the editor should hear about.
- While running, `smallwebwaf` notices a file that is edited, replaced or added. - While running, `smallwebwaf` notices a file that is edited, replaced or added.
It tells its own writes from an admin's by comparing the file with what it It tells its own writes from an admin's by comparing the file with what it
last read or wrote. An edit that parses is taken in at once: what the file last read or wrote. An edit that parses is taken in at once: what the file
says replaces what `smallwebwaf` held for that file. Removing a ban's entry says replaces what `smallwebwaf` held for that file. An edit to `bans.json`
from `bans.json` lifts the ban; adding an entry bans; setting `lifted` lifts makes, lifts or forgets bans as "Bans" describes. A change `smallwebwaf` made
it and keeps it. A change `smallwebwaf` made after the admin opened the file, after the admin opened the file, such as a new ban, is lost when the admin
such as a new ban, is lost when the admin saves over it. Most editors warn saves over it. Most editors warn when a file changed on disk while it was
when a file changed on disk while it was open. A file that is removed is open. A file that is removed is written again at its next write.
written again at its next write.
- An edit that does not parse never stops the running `smallwebwaf`. It keeps - An edit that does not parse never stops the running `smallwebwaf`. It keeps
the state it has and sets the edited file aside as `<name>.bad`, so the edit the state it has and sets the edited file aside as `<name>.bad`, so the edit
is kept. It writes the file again from memory, and names the file and the is kept. It writes the file again from memory, and names the file and the
@@ -999,8 +1000,6 @@ endpoints, what each takes and what each answers.
the error burst. A client guessing tokens is therefore banned once it passes the error burst. A client guessing tokens is therefore banned once it passes
`SWWAF_ERROR_BURST_THRESHOLD` guesses in a minute. `SWWAF_ERROR_BURST_THRESHOLD` guesses in a minute.
- A ban made through the endpoints starts at once, with the cause `admin`. - A ban made through the endpoints starts at once, with the cause `admin`.
Lifting through the endpoints lifts every active ban covering the address
given.
## Deployment ## Deployment
@@ -1017,8 +1016,10 @@ what the image holds and how each part is pinned.
need older versions of packages the image holds, and it moves forward with the need older versions of packages the image holds, and it moves forward with the
image's digest. The Dockerfile names the hash of each of the snapshot's image's digest. The Dockerfile names the hash of each of the snapshot's
`InRelease` files, which cover every package list and so every package, and `InRelease` files, which cover every package list and so every package, and
the build checks them before installing. The hashes are the amd64 archive's, the build checks them before installing. The live archive's `InRelease` files,
so the image is built for amd64 only. which `apt-get update` also fetches and the install does not use, are left out
of the check. The hashes are the amd64 archive's, so the image is built for
amd64 only.
- The Ubuntu image has no CA certificates. Without them Nix cannot download - The Ubuntu image has no CA certificates. Without them Nix cannot download
packages and `smallwebwaf` cannot reach GeoJS, so `ca-certificates` is packages and `smallwebwaf` cannot reach GeoJS, so `ca-certificates` is
installed by name. The snapshot itself is fetched over HTTPS with the CA installed by name. The snapshot itself is fetched over HTTPS with the CA
@@ -1032,15 +1033,14 @@ what the image holds and how each part is pinned.
own commands; busybox's `sv`, for one, looks for services elsewhere. An app own commands; busybox's `sv`, for one, looks for services elsewhere. An app
built on the image gets the same packages each time. Security fixes from built on the image gets the same packages each time. Security fixes from
Ubuntu and nixpkgs come with a newer `smallwebwaf` image, which an app takes Ubuntu and nixpkgs come with a newer `smallwebwaf` image, which an app takes
by changing the digest in its `FROM` line; the image moves to each new nixpkgs by changing the digest in its `FROM` line. The image moves to each new nixpkgs
release, every six months. release, every six months.
- runit supervises the two processes, with `runsvinit` as the entrypoint, as the - runit supervises the two processes, with `runsvinit` as the entrypoint, as the
owner's code style guide asks for service containers. No package carries owner's code style guide asks for service containers. No package carries
`runsvinit`, so the image builds it from its archived source, unchanged since `runsvinit`, so the image builds it from its archived source, unchanged since
2015, at a fixed commit hash, with a `go.mod` the build writes. `runsvinit` 2015, at a fixed commit hash, with a `go.mod` the build writes. `runsvinit`
starts runit's `runsvdir`, which runs the `run` script of each directory under starts runit's `runsvdir`, which runs the `run` script of each directory under
`/etc/service` and runs it again whenever it exits. Ubuntu's `sv` looks for `/etc/service`. Ubuntu's `sv` looks for services there too.
services there too.
- The image's `run` script for `smallwebwaf` waits one second and makes - The image's `run` script for `smallwebwaf` waits one second and makes
`SWWAF_STATE_DIR` and every file in it belong to the `smallwebwaf` user (uid `SWWAF_STATE_DIR` and every file in it belong to the `smallwebwaf` user (uid
and gid 65532). Then it starts `smallwebwaf` as that user with runit's and gid 65532). Then it starts `smallwebwaf` as that user with runit's
@@ -1072,19 +1072,20 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
script starts as root and hands over to its program's user with `chpst`. Both script starts as root and hands over to its program's user with `chpst`. Both
services start with the container's environment variables and write to the services start with the container's environment variables and write to the
container's output, which `docker logs` shows. container's output, which `docker logs` shows.
- When `smallwebwaf` or the app exits, runit starts it again a second later. The - When `smallwebwaf` or the app exits, runit runs its `run` script again, which
other process keeps running, and the container does not stop. While waits a second before starting it. The other process keeps running, and the
`smallwebwaf` is down, traefik answers `502`. While the app is down, container does not stop. While `smallwebwaf` is down, traefik answers `502`.
`smallwebwaf` answers `502`, which is not a client offence. While the app is down, `smallwebwaf` answers `502`, which is not a client
offence.
- Only four things stop `smallwebwaf`, all at start: - Only four things stop `smallwebwaf`, all at start:
- invalid configuration; - invalid configuration;
- a lookup database that is missing or unreadable; - a lookup database that is missing or unreadable;
- a rule file or state file that does not parse; - a rule file or state file that does not parse;
- an unwritable `SWWAF_STATE_DIR`. - an unwritable `SWWAF_STATE_DIR`.
- A `smallwebwaf` that stops at start is started again every second, logging its - A `smallwebwaf` that stops at start is therefore started again every second,
message each time, until the cause is fixed; the health check fails meanwhile. logging its message each time, until the cause is fixed; the health check
Once running, a failing GeoJS, reputation source or alert destination, or a fails meanwhile. Once running, a failing GeoJS, reputation source or alert
broken edit, never takes the protected service down. destination, or a broken edit, never takes the protected service down.
- `docker stop` sends `SIGTERM` to `runsvinit`, which has runit stop both - `docker stop` sends `SIGTERM` to `runsvinit`, which has runit stop both
services at once, each with `SIGTERM`, and exits once they have stopped. services at once, each with `SIGTERM`, and exits once they have stopped.
`smallwebwaf` stops taking requests, gives those in progress five seconds to `smallwebwaf` stops taking requests, gives those in progress five seconds to
@@ -1126,9 +1127,8 @@ beside the app's data directories, belonging to the `smallwebwaf` user. Without
a volume there, the files survive a restart of the container and are lost when a a volume there, the files survive a restart of the container and are lost when a
deploy replaces it. A volume mounted at `/var/lib/smallwebwaf`, named or a host deploy replaces it. A volume mounted at `/var/lib/smallwebwaf`, named or a host
directory, keeps them across deploys. A host directory mounted there needs no directory, keeps them across deploys. A host directory mounted there needs no
change of owner, since the `run` script gives it to the `smallwebwaf` user at change of owner, and files an earlier owner left in it can be read and replaced.
each start; files an earlier owner left in it can be read and replaced. The The volume is separate from the app's, holds a few tens of MiB at most with the
volume is separate from the app's, holds a few tens of MiB at most with the
defaults, and needs no backup beyond the host's own. The image declares no defaults, and needs no backup beyond the host's own. The image declares no
volume, since every app image built on it would inherit it. volume, since every app image built on it would inherit it.
@@ -1164,8 +1164,8 @@ and `script/gitea-requests` checks those notes.
## Planned, not built ## Planned, not built
Everything above is built. These two points are specified as later work, and These two points are specified as later work, and nothing in the code does them
nothing in the code does them yet: yet:
- Planned: sharing bans between the instances of several apps, by running one - Planned: sharing bans between the instances of several apps, by running one
CrowdSec engine per host that every instance reports its bans to. It is CrowdSec engine per host that every instance reports its bans to. It is
+8 -5
View File
@@ -78,11 +78,14 @@ on one line:
- `instance`: `SWWAF_INSTANCE_NAME`. `time`: when the alert was raised. `event`: - `instance`: `SWWAF_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
one of the events above. one of the events above.
- `client`: the address whose request raised the alert. `netblock`: the ban's - `client`: the address whose request raised the alert. Empty for
netblock, the netblock an anomaly counted, or, for the other events, the `source_failure` and `file_error`.
client as rate limiting counts it: an IPv4 address as a /32, or an IPv6 group. - `netblock`: empty for `source_failure` and `file_error`; otherwise one of:
Both are empty for `source_failure` and `file_error`. - the ban's netblock;
- `asn`, `as_name` and `country`: the client's as known when the alert was - the netblock an anomaly counted;
- for the other events, the client as rate limiting counts it: an IPv4
address as a /32, or an IPv6 group.
- `asn`, `as_name` and `country`: the client's, as known when the alert was
raised. They are empty when GeoJS had not answered yet. raised. They are empty when GeoJS had not answered yet.
- `reason`: a short sentence, such as - `reason`: a short sentence, such as
`requests per minute over the limit of 1000` or `listed by a blocklist`. `requests per minute over the limit of 1000` or `listed by a blocklist`.
+38 -34
View File
@@ -21,10 +21,10 @@ checked again when gitea or the Core Rule Set changes version.
- a release attachment. - a release attachment.
At the defaults, an upload larger than 100 MiB or taking more than 60 At the defaults, an upload larger than 100 MiB or taking more than 60
seconds is cut off, and no one is banned for it. "Size and time limits" in seconds is cut off ("Size and time limits" in `SPEC.md`). A gitea that takes
`SPEC.md` gives the answers the client gets. A gitea that takes large large uploads needs `SWWAF_REQUEST_MAX_BYTES`,
uploads needs `SWWAF_REQUEST_MAX_BYTES`, `SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_UPSTREAM_REQUEST_TIMEOUT` raised
`SWWAF_UPSTREAM_REQUEST_TIMEOUT` raised to fit. to fit.
- Archive download and blame or history pages are what scrapers hammer. The - Archive download and blame or history pages are what scrapers hammer. The
request limits do most of the work there. request limits do most of the work there.
@@ -63,15 +63,15 @@ toward). The request log names the rule in `waf_rule_ids`, which
### A query string that reads as an attack ### A query string that reads as an attack
Most often this is a search. Every query parameter is checked against the Core Most often this is a search. A search term is a query parameter, which the Core
Rule Set's lists of system files and commands, except the ten named under Rule Set checks against its lists of system files and commands (see the fourth
"Attack detection" in `SPEC.md`. A release attachment uploaded through the API change under "Attack detection" in `SPEC.md`). A release attachment uploaded
as `docker-compose.yml` is refused the same way. Examples: through the API as `docker-compose.yml` is refused the same way. Examples:
- a name on those lists, such as `package.json`, `.gitignore` or - a name on those lists, such as `package.json`, `.gitignore` or
`docker-compose.yml`; `docker-compose.yml`;
- text that starts with a command name, or whose first word has one right after - text that starts with a command name, such as `python3` or `ssh key`, or has
a `/`, such as `python3`, `ssh key` or `feat/docker-support`; one right after a `/` in its first word, such as `feat/docker-support`;
- a shell command with its options or a system path (`ls -la`, `sed -i`, - a shell command with its options or a system path (`ls -la`, `sed -i`,
`/bin/sh`), or a command in backticks; `/bin/sh`), or a command in backticks;
- script code (`fetch(`, `${VAR}`, `process.env`), HTML (`<img src=`) or an SQL - script code (`fetch(`, `${VAR}`, `process.env`), HTML (`<img src=`) or an SQL
@@ -81,15 +81,15 @@ as `docker-compose.yml` is refused the same way. Examples:
### A branch, tag or file name that starts with a command name ### A branch, tag or file name that starts with a command name
The Core Rule Set's list of commands (932260) holds names such as `docker-`, The Core Rule Set's list of commands (932260) holds names such as `docker-`,
`python3`, `ansible`, `base64` and `whoami`. A branch, tag or file name that `python3`, `ansible`, `base64` and `whoami`. A query parameter holding a branch,
starts with one is refused. So is one that has such a name right after a `/`, tag or file name that starts with one is refused. So is one that has such a name
such as `feat/docker-support`, `fix/docker-build` or right after a `/`, such as `feat/docker-support`, `fix/docker-build` or
`renovate/docker-build-push-action-6.x`. gitea's own pages send the name in a `renovate/docker-build-push-action-6.x`. gitea's own pages send the name in a
query parameter that 930120, 932160 and 932260 still check: query parameter that 930120, 932160 and 932260 still check:
- `refSubUrl`, the branch or tag of a directory listing. It is sent when the - `refSubUrl`, the branch or tag of a directory listing. A listing sends it when
listing asks for its entries' last commits in a second request, which it does it asks for its entries' last commits in a second request. It does so whenever
whenever gitea takes more than a second to work them out; gitea takes more than a second to work them out;
- `name`, when a branch is deleted or restored on the branches page; - `name`, when a branch is deleted or restored on the branches page;
- `tag`, when a release is started from a tag; - `tag`, when a release is started from a tag;
- `template`, the file of an issue template, when an issue is opened from it; - `template`, the file of an issue template, when an issue is opened from it;
@@ -110,9 +110,9 @@ What a visitor sees, for a branch named `docker-build`:
### A file name in a query parameter that trips the code rules ### A file name in a query parameter that trips the code rules
The Core Rule Set's rules for Java, script, PowerShell and SQL find what they The Core Rule Set's rules for Java, script, PowerShell and SQL find what they
look for in some file names. This holds in the parameters `smallwebwaf` leaves look for in some file names. This holds too in the ten query parameters that
out of 930120, 932160 and 932260 too (see the changes to the Core Rule Set under 930120, 932160 and 932260 do not check (see the fourth change under "Attack
"Attack detection" in `SPEC.md`). Examples: detection" in `SPEC.md`). Examples:
- `runtime` in OpenJDK's `src/java.base/share/classes/java/lang/Runtime.java` - `runtime` in OpenJDK's `src/java.base/share/classes/java/lang/Runtime.java`
(944110); (944110);
@@ -129,9 +129,9 @@ For such a file, these requests are refused:
- the API's call for the contents of files that names it in its query (`body`); - the API's call for the contents of files that names it in its query (`body`);
- the commit graph of that file alone (`file`); - the commit graph of that file alone (`file`);
- the hidden lines of its diff (`path`), which stay hidden; - the hidden lines of its diff (`path`), which stay hidden;
- the sign-in page gitea sends a visitor to on the way to a page of the file - the sign-in page, for a visitor who opens a page of the file that needs
that needs signing in, such as its edit page (`redirect_to`). The visitor can signing in, such as its edit page (`redirect_to`). The visitor can still sign
still sign in by opening the sign-in page itself. in by opening the sign-in page itself.
### A path ### A path
@@ -143,25 +143,30 @@ For such a file, these requests are refused:
`process.env.d.ts` does. `process.env.d.ts` does.
- A path with a directory named `...` (930100, 930110), such as `.../notes.txt`. - A path with a directory named `...` (930100, 930110), such as `.../notes.txt`.
For such a file, the page of the file, its raw file, its history, its blame and For such a file, these are refused:
its edit page are refused. The directory that holds it lists it, and a clone
carries it. - the page of the file;
- its raw file;
- its history and its blame;
- its edit page.
The directory that holds it lists it, and a clone carries it.
### A container image push that reuses a layer ### A container image push that reuses a layer
A push can reuse a layer of an image docker has pushed to that gitea before. A push can reuse a layer of an image docker has pushed to that gitea before.
docker then asks gitea to take the layer from that image, naming it in the query docker then asks gitea to take the layer from that image, naming it in the query
parameter `from`, which 932260 checks. The request is refused when the image's parameter `from`, which 932260 checks. The request is refused when the image's
owner starts with an entry on the list of commands. So is one whose name after owner starts with an entry on the list of commands. It is refused too when the
the owner does. Examples are `owner/python3-app`, `owner/docker-base`, and any image's name after the owner starts with one. Examples are `owner/python3-app`,
image of an owner named `ansible` or `docker-team`. `owner/docker-base`, and any image of an owner named `ansible` or `docker-team`.
What happens next depends on docker's image store. A docker that keeps its What happens next depends on docker's image store. A docker that keeps its
images in containerd's image store fails the push, and fails each new try the images in containerd's image store fails the push, and fails each new try the
same way. A new install of Docker Engine 29 does this. A docker that keeps them same way. A new install of Docker Engine 29 does this. A docker that keeps them
in its own older image store uploads the layer instead. The push then succeeds in its own older image store uploads the layer instead. The push then succeeds
after one refusal for each layer it reuses, and each of those refusals counts after one refusal for each layer it reuses, and each of those refusals counts
toward the error burst. toward the error burst ("Attack detection" in `SPEC.md`).
### Artifact uploads from `actions/upload-artifact@v3` ### Artifact uploads from `actions/upload-artifact@v3`
@@ -169,9 +174,9 @@ This action sends the header `Content-Range`, which the Core Rule Set refuses
(920450). The action sends two files at a time, does not retry a `403`, and (920450). The action sends two files at a time, does not retry a `403`, and
sends no further file once one is refused. So an upload is refused once or sends no further file once one is refused. So an upload is refused once or
twice, however many files it holds, and the step fails. One upload does not ban twice, however many files it holds, and the step fails. One upload does not ban
the runner. More than 30 such refusals from its address within a minute, as when the runner. The error burst ("Attack detection" in `SPEC.md`) bans it after more
many jobs upload at once, ban it through the error burst. than 30 such refusals from its address in a minute, as when many jobs upload at
`actions/upload-artifact@v4` does not send the header. once. `actions/upload-artifact@v4` does not send the header.
## What body inspection refuses ## What body inspection refuses
@@ -193,8 +198,7 @@ Of the requests above, the script sends:
code rules", the API's list of its commits; code rules", the API's list of its commits;
- for `Runtime.java` alone, the commit graph of that file, the hidden lines of - for `Runtime.java` alone, the commit graph of that file, the hidden lines of
its diff, and the API's call for the contents of files that names it; its diff, and the API's call for the contents of files that names it;
- the sign-in page gitea sends a visitor to on the way to the edit page of - the sign-in page for a visitor who opens the edit page of `document.write.js`;
`document.write.js`;
- the pages of `java.lang.String.md`, - the pages of `java.lang.String.md`,
`META-INF/services/org.apache.commons.logging.LogFactory`, `process.env.d.ts` `META-INF/services/org.apache.commons.logging.LogFactory`, `process.env.d.ts`
and `.../notes.txt`; and `.../notes.txt`;
+3 -2
View File
@@ -31,6 +31,7 @@ is pinned that way.
- nixpkgs: the release file `nixexprs.tar.xz` of - nixpkgs: the release file `nixexprs.tar.xz` of
`nixos-26.05.11045.774debe7a0d1` on `releases.nixos.org`, named with its `nixos-26.05.11045.774debe7a0d1` on `releases.nixos.org`, named with its
SHA-256 hash. SHA-256 hash.
- `runsvinit`: the archived repository, unchanged since 2015, at tag `v2.0.0`, - `runsvinit`: the archived repository, unchanged since 2015, at its last commit
by commit hash, with a `go.mod` the build writes. (`b4b2c78`, eight commits after `v2.0.0`), by commit hash, with a `go.mod` the
build writes.
- The Go image that builds `smallwebwaf`: by digest. - The Go image that builds `smallwebwaf`: by digest.
+3 -5
View File
@@ -1,10 +1,8 @@
# The metrics # The metrics
`GET /_smallwebwaf/metrics` answers in the Prometheus text format. The The metrics `GET /_smallwebwaf/metrics` answers with, as "Metrics endpoint" in
requirements are under "Metrics endpoint" in [`SPEC.md`](../SPEC.md); this file [`SPEC.md`](../SPEC.md) describes it. Prometheus keeps the `instance` label as
lists the metrics. Every metric carries the label `instance`, set to `exported_instance` unless the scrape sets `honor_labels: true`.
`SWWAF_INSTANCE_NAME`; Prometheus keeps it as `exported_instance` unless the
scrape sets `honor_labels: true`. No metric carries a client's address.
## Traffic ## Traffic
+23 -12
View File
@@ -12,9 +12,13 @@ sent to a syslog server. A field that does not apply to a request is left out.
## Every line ## Every line
Every line has `type` (`request`; the process's own messages have `process`), Every line has:
the standard web log fields, `request_id`, `peer_ip`, `client_group`, `asn`,
`as_name`, `country`, `action` and `duration_total`. - `type`: `request` (the process's own messages have `process`);
- the standard web log fields;
- `request_id`, `peer_ip` and `client_group`;
- `asn`, `as_name` and `country`;
- `action` and `duration_total`.
## Standard web log fields ## Standard web log fields
@@ -28,7 +32,7 @@ the standard web log fields, `request_id`, `peer_ip`, `client_group`, `asn`,
## Request detail ## Request detail
- `request_id`: the `X-Request-ID` a trusted proxy sent, or a new random one of - `request_id`: the `X-Request-ID` a trusted proxy sent, or a new random one of
26 letters and digits. It goes to the app in `X-Request-ID`. 26 letters and digits.
- `peer_ip`: the TCP peer, normally traefik. - `peer_ip`: the TCP peer, normally traefik.
- `forwarded_for`: the `X-Forwarded-For` header as received. - `forwarded_for`: the `X-Forwarded-For` header as received.
- `client_group`: the client as rate limiting counts it, an IPv4 address as a - `client_group`: the client as rate limiting counts it, an IPv4 address as a
@@ -73,15 +77,22 @@ the standard web log fields, `request_id`, `peer_ip`, `client_group`, `asn`,
rate limits and the setting that lowered it, when one did. `bytes_percent` and rate limits and the setting that lowered it, when one did. `bytes_percent` and
`bytes_percent_setting` do the same for the byte limits. `bytes_percent_setting` do the same for the byte limits.
- `counts`: the client's totals as rate limiting counts them, this request - `counts`: the client's totals as rate limiting counts them, this request
included. It is left out when rate limiting did not count the request. A count included. It is left out when rate limiting did not count the request. It
can have a fraction, since each window is counted in two buckets with the holds:
earlier one weighted:
- `minute`, `hour` and `day`: requests; - `minute`, `hour` and `day`: requests;
- `minute_bytes`, `hour_bytes` and `day_bytes`: bytes. - `minute_bytes`, `hour_bytes` and `day_bytes`: bytes.
- `limit_hit`: the limit broken: `minute`, `hour` or `day` for a rate limit;
`minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; or A count can have a fraction (see "Counting method" in
`error_burst`. A request whose bytes broke a limit was passed on whole, so its [`SPEC.md`](../SPEC.md)).
`action` stays `forward`.
- `limit_hit`: the limit broken:
- `minute`, `hour` or `day` for a rate limit;
- `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit;
- `error_burst`.
A request whose bytes broke a limit was passed on whole, so its `action`
stays `forward`.
- `rule_ids`: the rule file rules that matched, in order, up to the one that - `rule_ids`: the rule file rules that matched, in order, up to the one that
refused. refused.
- `waf_score` and `waf_rule_ids`: the Core Rule Set's score and the rules that - `waf_score` and `waf_rule_ids`: the Core Rule Set's score and the rules that
@@ -91,7 +102,7 @@ the standard web log fields, `request_id`, `peer_ip`, `client_group`, `asn`,
- the CrowdSec decision list's URL; - the CrowdSec decision list's URL;
- DNS blocklist zones; - DNS blocklist zones;
- `abuseipdb`. - `abuseipdb`.
- `offence`: the kind of offence recorded, when one was. - `offence`: `limit`, for a request that broke a limit.
- `ban_expires`: when the client's ban ends, for a request that made one or was - `ban_expires`: when the client's ban ends, for a request that made one or was
refused under one: a time, or `permanent`. refused under one: a time, or `permanent`.
+17 -18
View File
@@ -2,8 +2,7 @@
Every setting, its default and the values it takes, in the groups of "Settings" Every setting, its default and the values it takes, in the groups of "Settings"
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
that take `off`, the `_FILE` form, and what each group does and why. A setting that take `off`, the `_FILE` form, and what each group does and why.
set to a value its entry does not allow stops the start.
## Forms of values ## Forms of values
@@ -27,8 +26,9 @@ set to a value its entry does not allow stops the start.
- `SWWAF_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `http` or - `SWWAF_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `http` or
`https` URL with a host and an optional port, and nothing more. `https` URL with a host and an optional port, and nothing more.
- `SWWAF_LISTEN_ADDR` (default `:8080`): where `smallwebwaf` listens, an - `SWWAF_LISTEN_ADDR` (default `:8080`): where `smallwebwaf` listens: `:` and a
optional host and a port. The host stays empty. port, such as `:9000`. Leave the host out (see "Ports" in
[`SPEC.md`](../SPEC.md)).
- `SWWAF_INSTANCE_NAME` (default: the container's host name): the `instance` - `SWWAF_INSTANCE_NAME` (default: the container's host name): the `instance`
name in every log line, alert and metric, such as `fsn1app1/gitea`. Valid name in every log line, alert and metric, such as `fsn1app1/gitea`. Valid
UTF-8. No control character while `SWWAF_ALERT_NTFY_URL` is set. 1 to 48 UTF-8. No control character while `SWWAF_ALERT_NTFY_URL` is set. 1 to 48
@@ -42,8 +42,8 @@ set to a value its entry does not allow stops the start.
client's group, from 32 to 128. client's group, from 32 to 128.
- `SWWAF_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole - `SWWAF_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
number above zero. number above zero.
- `SWWAF_MAX_BANS` (default `5000`): the most bans `smallwebwaf` made that are - `SWWAF_MAX_BANS` (default `5000`): how many of its own bans `smallwebwaf`
kept, a whole number above zero. keeps, a whole number above zero.
## Tokens ## Tokens
@@ -90,8 +90,9 @@ set to a value its entry does not allow stops the start.
## Static lists ## Static lists
- `SWWAF_ALLOW_NETS` (default empty): netblocks whose clients are refused only - `SWWAF_ALLOW_NETS` (default empty): netblocks you trust, such as your
for a size or time limit or a missing or wrong token, such as your monitoring. monitoring. Their clients are refused only for a size or time limit or a
missing or wrong token.
- `SWWAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are - `SWWAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients are
exempt from rate limiting and the byte limits. exempt from rate limiting and the byte limits.
- `SWWAF_DENY_NETS` (default empty): netblocks whose clients are always refused. - `SWWAF_DENY_NETS` (default empty): netblocks whose clients are always refused.
@@ -150,7 +151,7 @@ set to a value its entry does not allow stops the start.
such as `cn,ru,kp`. such as `cn,ru,kp`.
- `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only - `SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES` (default empty): when set, the only
countries whose clients get through, such as `us,de`. A country on both lists countries whose clients get through, such as `us,de`. A country on both lists
is refused. stops the start.
## Biased thresholds ## Biased thresholds
@@ -164,8 +165,8 @@ set to a value its entry does not allow stops the start.
- `SWWAF_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the - `SWWAF_UNKNOWN_LIMIT_PERCENT` (default `100`): the percentage for a client the
lookup cannot place. lookup cannot place.
- `SWWAF_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a - `SWWAF_ASN_LIMIT_PERCENT_URL` (default unset): the `http` or `https` URL of a
text file of `AS<number>:<percent>` lines, one to a line. A URL that is also text file with one AS number and percentage on each line, such as
in `SWWAF_BLOCKLIST_URLS` is refused. `AS64496:50`. A URL that is also in `SWWAF_BLOCKLIST_URLS` is refused.
## Attack detection ## Attack detection
@@ -260,8 +261,8 @@ set to a value its entry does not allow stops the start.
`ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`): `ban,permanent_ban,waf_block,anomaly,reputation_hit,source_failure,file_error`):
the event types alerts are sent for, from those in [`alerts.md`](alerts.md) the event types alerts are sent for, from those in [`alerts.md`](alerts.md)
but `summary`. Any other name is refused. but `summary`. Any other name is refused.
- `SWWAF_ALERT_COOLDOWN` (default `15m`): how long a repeat of an alert is held - `SWWAF_ALERT_COOLDOWN` (default `15m`): how long after an alert is sent its
back. A duration or `off`. repeats are held back. A duration or `off`.
- `SWWAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A - `SWWAF_ALERT_MAX_PER_HOUR` (default `60`): the most alerts sent in an hour. A
whole number above zero, or `off`. whole number above zero, or `off`.
@@ -294,10 +295,8 @@ as `REQUESTS_PER_MINUTE`:
## Settings given as files ## Settings given as files
Add `_FILE` to a setting's name to read its value from a file: To give a token as a file, as "Tokens" under "Deployment" in
`SWWAF_METRICS_TOKEN_FILE` names the file that holds `SWWAF_METRICS_TOKEN`. To [`SPEC.md`](../SPEC.md) describes, make the file on the host:
keep a token from the app, make the file on the host, owned by uid 65532 with
mode `0400`:
```sh ```sh
mkdir -p /srv/app/tokens mkdir -p /srv/app/tokens
@@ -306,7 +305,7 @@ chown 65532:65532 /srv/app/tokens/metrics
chmod 0400 /srv/app/tokens/metrics chmod 0400 /srv/app/tokens/metrics
``` ```
Mount its directory into the container read-only, and name the file: Then mount its directory into the container read-only, and name the file:
```sh ```sh
docker run \ docker run \
+19 -15
View File
@@ -15,8 +15,8 @@ those `smallwebwaf` made. Per entry:
- `lifted`: when an admin lifted it, if one did; - `lifted`: when an admin lifted it, if one did;
- `notes`: the notes below. - `notes`: the notes below.
To ban a netblock, add an entry with its `netblock`, `start` and `expires`. An entry an admin adds needs `netblock`, `start` and `expires`; `reason`,
`reason`, `notes` and `cause` may be left out; the cause is then `admin`: `notes` and `cause` may be left out, and the cause is then `admin`:
```json ```json
{ {
@@ -32,20 +32,23 @@ To ban a netblock, add an entry with its `netblock`, `start` and `expires`.
} }
``` ```
To lift a ban, add `"lifted"` with the time you lift it, such as "Bans" in [`SPEC.md`](../SPEC.md) says how an edit lifts, keeps or forgets a
`"lifted": "2026-10-06T13:00:00Z"`. To keep a ban `smallwebwaf` made, set its ban. The notes:
`cause` to `admin`. To forget a ban, delete its entry.
The notes:
- `asn`, `as_name` and `country`: the client's, added when the lookup answers - `asn`, `as_name` and `country`: the client's, added when the lookup answers
(empty when lookups are off); (empty when lookups are off);
- what was broken: the rule id and target that matched, or the trap path, or the - for a broken limit, `kind` (`requests`, `bytes` or `refusals`), `limit`,
limit, its window and the count reached; `window` and `count`: what the limit was on, the limit, its window and the
- the client's percentage of the limit, with the setting that gave it; count reached;
- the reputation sources the client is on; - `limit_percent` and `limit_percent_setting`: the client's percentage of that
- `request`: the request that caused the ban: time, method, host, path with its limit and the setting that gave it, when a setting lowered it;
query string, status and user agent, each text cut to 256 bytes; - for a clear sign of attack, `rule_id` and `target`, or `trap_path`;
- `reputation`: the sources the client was on, each with its `source` and, for
AbuseIPDB, its `score`;
- `request`: the request that caused the ban, each text cut to 256 bytes:
- `time`, `method` and `host`;
- `path`, with its query string;
- `status` and `user_agent`;
- `requests` and `refused`: the netblock's requests since it was first seen, and - `requests` and `refused`: the netblock's requests since it was first seen, and
how many this ban has refused so far, kept up to date while the ban lasts; how many this ban has refused so far, kept up to date while the ban lasts;
- `earlier_bans`: how many earlier bans the netblock had, by cause. - `earlier_bans`: how many earlier bans the netblock had, by cause.
@@ -93,8 +96,9 @@ GeoJS answers, one per line:
`SWWAF_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of `SWWAF_MAX_TRACKED_CLIENTS`. Written every 15 minutes, that is under 2 GiB of
disk writes a day. disk writes a day.
- `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at - `bans.json`: about 1.2 KiB per ban, at most about 2.5 KiB, so about 6 MiB at
the default `SWWAF_MAX_BANS`. With a full file, a hundred new bans a day come the default `SWWAF_MAX_BANS`. Written every 15 minutes, a full file comes to
to about 600 MiB of writes. about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB
more.
- `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when - `lookups.json`: about 150 bytes per answer, under 1.5 GiB of writes a day when
full. full.
- `reputation.json` and `alerts.json`: usually a few MiB or less. - `reputation.json` and `alerts.json`: usually a few MiB or less.