Compare commits

..
Author SHA1 Message Date
sneak 1dbe17e594 README rewritten from scratch so a newcomer can follow it (closes #154, closes #155)
check / check (push) Canceled after 0s
The old README opened with project history and one list of a hundred
settings. The new one leads with what smallwebwaf is, what it protects
against, how to build and deploy it, what happens to a request, the
settings most deployments touch, how to check it works, the entrypoints,
the rationale and the design, as REPO_POLICIES.md requires. The reference
lists, every setting, the rule file format, the endpoints, the request
log's fields, the alert events, the metrics, the state files, the image
and the gitea notes, move to files under docs/, each linked from the
README, with SPEC.md as the one place for behaviour. Every statement was
checked against the code on next.

Model: fable-5-1
2026-10-22 22:29:53 +00:00
8 changed files with 150 additions and 123 deletions

No files matched your search

+39 -32
View File
@@ -30,9 +30,9 @@ reference documents under [`docs/`](docs/) are listed under "Documents" below.
of your own, or an entry in `SWWAF_TRAP_PATHS`, bans those the same way. of your own, or an entry in `SWWAF_TRAP_PATHS`, bans those the same way.
- Attacks carried in a request's URL or headers, such as SQL injection and path - Attacks carried in a request's URL or headers, such as SQL injection and path
traversal. The OWASP Core Rule Set is a published set of rules that recognise traversal. The OWASP Core Rule Set is a published set of rules that recognise
common web attacks. Coraza, a Go engine that runs those rules, inspects each common web attacks. Coraza, a Go engine, runs those rules (step 11 under "What
request the earlier checks let through. A request the rules take for an attack happens to a request" below). A request the rules take for an attack is
is refused. refused.
- Abuse from networks and countries you do not want. You can: - Abuse from networks and countries you do not want. You can:
- refuse whole netblocks and countries; - refuse whole netblocks and countries;
- give the clients of chosen networks and countries a fraction of every - give the clients of chosen networks and countries a fraction of every
@@ -132,8 +132,9 @@ docker run --detach --name app \
With no setting, the address of every new visitor is sent to GeoJS, a web With no setting, the address of every new visitor is sent to GeoJS, a web
service, to find its country and network. `SWWAF_LOOKUP_SOURCE=off` sends service, to find its country and network. `SWWAF_LOOKUP_SOURCE=off` sends
nothing. `SWWAF_LOOKUP_SOURCE=file` looks addresses up in a database file on nothing. `SWWAF_LOOKUP_SOURCE=file`, with `SWWAF_LOOKUP_DB_PATH` naming a
your own host (see "Lookup of AS number and country" in [`SPEC.md`](SPEC.md)). database file on your own host, looks addresses up in that file (see "Lookup of
AS number and country" in [`SPEC.md`](SPEC.md)).
`deploy/example-app` is such an app. `make example-app` builds it and checks it `deploy/example-app` is such an app. `make example-app` builds it and checks it
(see "Checking that it works" below). To run `smallwebwaf` outside the image (see "Checking that it works" below). To run `smallwebwaf` outside the image
@@ -149,9 +150,10 @@ request that counts against its client in its history. It is one of:
- a broken limit; - a broken limit;
- a refusal by a rule, by the Core Rule Set or for a missing or wrong token. - a refusal by a rule, by the Core Rule Set or for a missing or wrong token.
`smallwebwaf` handles each request in this order. The first step that refuses it `smallwebwaf` handles each request in this order. A request one step refuses
ends the handling. Nothing of a refused request reaches the app. "What happens skips the steps after it, apart from step 13. Nothing of a refused request
to a request" in [`SPEC.md`](SPEC.md) gives each step in full. reaches the app. "What happens to a request" in [`SPEC.md`](SPEC.md) gives each
step in full.
1. `GET /_smallwebwaf/healthz` is answered `200` with `ok` at once, before any 1. `GET /_smallwebwaf/healthz` is answered `200` with `ok` at once, before any
check and without asking the app. check and without asking the app.
@@ -187,18 +189,18 @@ to a request" in [`SPEC.md`](SPEC.md) gives each step in full.
10. The rule files are checked. A `log` rule notes the match. A `block` rule 10. The rule files are checked. A `log` rule notes the match. A `block` rule
refuses the request. A `ban` rule refuses it and bans the client, as a clear refuses the request. A `ban` rule refuses it and bans the client, as a clear
sign of attack. sign of attack.
11. The Core Rule Set inspects the request's method, URL and headers, and the 11. The Core Rule Set inspects the request's method, URL and headers, and, once
body too once `SWWAF_WAF_BODY_LIMIT` is set. A request it takes for an `SWWAF_WAF_BODY_LIMIT` is set, form, JSON and XML bodies. A request it takes
attack is refused while `SWWAF_WAF_MODE` is `block`, the default, and let for an attack is refused while `SWWAF_WAF_MODE` is `block`, the default, and
through while it is `detect`. It bans no one by itself. let through while it is `detect`. It bans no one by itself.
12. A request under `/_smallwebwaf/` is answered here by `smallwebwaf`, after 12. A request under `/_smallwebwaf/` is answered here by `smallwebwaf`, after
its token is checked. Any other request is passed to the app. The app's its token is checked. Any other request is passed to the app. The app's
answer is streamed back within the size and time limits. answer is streamed back within the size and time limits.
13. Once the answer has ended, the request's bytes are counted for the byte 13. Once the answer has ended, a request passed to the app has its bytes counted
limits. If a rule, a trap path, the Core Rule Set or a token check refused for the byte limits. If a rule, a trap path, the Core Rule Set or a token
it, the refusal is counted for the error burst. The client's history and the check refused the request, the refusal is counted for the error burst. The
anomaly counters are updated. More than `SWWAF_ERROR_BURST_THRESHOLD` client's history and the anomaly counters are updated. More than
refusals of one client in a minute ban it. `SWWAF_ERROR_BURST_THRESHOLD` refusals of one client in a minute ban it.
In `observe` mode (`SWWAF_MODE=observe`), `smallwebwaf` makes no ban of its own. In `observe` mode (`SWWAF_MODE=observe`), `smallwebwaf` makes no ban of its own.
It refuses a request only for a size or time limit, or for a missing or wrong It refuses a request only for a size or time limit, or for a missing or wrong
@@ -209,27 +211,32 @@ configuration before enforcing it.
What a refused visitor sees: What a refused visitor sees:
- `SWWAF_BAN_RESPONSE`, by default `403` with the plain text `Forbidden`. It can - `SWWAF_BAN_RESPONSE`, by default `403` with the plain text `Forbidden`. It can
be `429` instead, or `close`, which closes the connection without an answer. be `429` instead, or `close`, which closes the connection, so that traefik
It answers: answers `502`. It answers:
- a ban or a broken rate limit; - a ban or a broken rate limit;
- a `ban` rule or a trap path; - a `ban` rule or a trap path;
- `SWWAF_DENY_NETS` and the country lists; - `SWWAF_DENY_NETS` and the country lists;
- a reputation source whose action is `deny`. - a reputation source whose action is `deny`.
- `403` for a `block` rule and the Core Rule Set. - `403` for a `block` rule and the Core Rule Set.
- `413` or `408` when the client passes a size or time limit, `502` or `504` - For a size or time limit:
when the app does. - `431` for headers over their limit;
- `413` for a body over its limit;
- `408` for a client too slow to send its body;
- `502` or `504` when the app passes a limit or cannot be reached.
- `401` from an endpoint of `smallwebwaf`'s own, for a request without its - `401` from an endpoint of `smallwebwaf`'s own, for a request without its
token. token.
## Settings you are likely to touch ## Settings you are likely to touch
Each setting is an environment variable of the app's container, such as
`docker run -e SWWAF_MODE=observe ...`, read at start.
[`docs/settings.md`](docs/settings.md) lists every setting, its default and the [`docs/settings.md`](docs/settings.md) lists every setting, its default and the
values it takes. The ones most deployments touch: values it takes. The ones most deployments touch:
- `SWWAF_MODE=observe`: log what would have been refused, refuse almost nothing, - `SWWAF_MODE=observe`: log what would have been refused, refuse almost nothing,
ban no one. For trying a configuration. ban no one. For trying a configuration.
- `SWWAF_ALLOW_NETS`: netblocks whose clients are refused only for a size or - `SWWAF_ALLOW_NETS`: netblocks you trust, such as your monitoring. Their
time limit or a missing or wrong token, such as your monitoring. clients are refused only for a size or time limit or a missing or wrong token.
- `SWWAF_RATE_LIMIT_EXEMPT_NETS`: netblocks exempt from rate limiting and the - `SWWAF_RATE_LIMIT_EXEMPT_NETS`: netblocks exempt from rate limiting and the
byte limits, such as a machine that talks to the app all day. byte limits, such as a machine that talks to the app all day.
- `SWWAF_TRAP_PATHS`: paths only scanners ask for, such as - `SWWAF_TRAP_PATHS`: paths only scanners ask for, such as
@@ -237,7 +244,8 @@ values it takes. The ones most deployments touch:
the client. the client.
- `SWWAF_WAF_DISABLED_RULES`: Core Rule Set rule ids to switch off when a rule - `SWWAF_WAF_DISABLED_RULES`: Core Rule Set rule ids to switch off when a rule
refuses your app's ordinary requests. The log line names the rules a request refuses your app's ordinary requests. The log line names the rules a request
matched. matched. Keep the six default ids in a list you give (see
[`docs/settings.md`](docs/settings.md)).
- `SWWAF_REQUEST_MAX_BYTES`, `SWWAF_CLIENT_REQUEST_TIMEOUT` and - `SWWAF_REQUEST_MAX_BYTES`, `SWWAF_CLIENT_REQUEST_TIMEOUT` and
`SWWAF_UPSTREAM_REQUEST_TIMEOUT`: raise them for an app that takes uploads `SWWAF_UPSTREAM_REQUEST_TIMEOUT`: raise them for an app that takes uploads
over 100 MiB or slower than 60 seconds. over 100 MiB or slower than 60 seconds.
@@ -261,9 +269,8 @@ values it takes. The ones most deployments touch:
`SWWAF_MODE=observe` and ask for `/.env`. The request reaches the app. Its log `SWWAF_MODE=observe` and ask for `/.env`. The request reaches the app. Its log
line has `"would_action":"banned"` and `"rule_ids":["env-file"]`. In `enforce` line has `"would_action":"banned"` and `"rule_ids":["env-file"]`. In `enforce`
mode the same request bans your address for seven days, and your next request mode the same request bans your address for seven days, and your next request
makes the ban permanent. Lift the ban by editing `bans.json` or through the makes the ban permanent. Lift the ban by editing `bans.json` (see "Bans" in
ban endpoint (see "Bans" in [`SPEC.md`](SPEC.md) and [`SPEC.md`](SPEC.md)).
[`docs/admin-endpoints.md`](docs/admin-endpoints.md)).
- With `SWWAF_METRICS_TOKEN` set, - With `SWWAF_METRICS_TOKEN` set,
`curl -H "Authorization: Bearer $TOKEN" https://<site>/_smallwebwaf/metrics` `curl -H "Authorization: Bearer $TOKEN" https://<site>/_smallwebwaf/metrics`
answers with the metrics. answers with the metrics.
@@ -285,8 +292,8 @@ the `Makefile` targets are thin shims that call them. The scripts are POSIX sh.
The tests and the linter run in Docker, as phases of the `Dockerfile`, and never The tests and the linter run in Docker, as phases of the `Dockerfile`, and never
on the host. on the host.
- `script/bootstrap`: installs what the other scripts need on the host. - `script/bootstrap`: installs on the host what the other scripts need, with
`make bootstrap`. They need: `make bootstrap`:
- `make`, `git` and `curl`; - `make`, `git` and `curl`;
- Go, for `gofmt`; - Go, for `gofmt`;
- node, yarn and prettier. - node, yarn and prettier.
@@ -416,9 +423,9 @@ another port, the health check's timing, and the forwarded headers.
## TODO ## TODO
- Everything `SPEC.md` describes is built. - Planned, as "Planned, not built" in [`SPEC.md`](SPEC.md) describes:
- Two points `SPEC.md` defers until there is experience to go on: sharing bans - sharing bans between the instances of several apps;
between the instances of several apps, and sending the log over RELP. - sending the log over RELP.
## Documents ## Documents
+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.