Compare commits

..
Author SHA1 Message Date
sneak 91438f54da 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 21:56:16 +00:00
8 changed files with 123 additions and 150 deletions

No files matched your search

+32 -39
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, runs those rules (step 11 under "What common web attacks. Coraza, a Go engine that runs those rules, inspects each
happens to a request" below). A request the rules take for an attack is request the earlier checks let through. A request the rules take for an attack
refused. is 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,9 +132,8 @@ 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`, with `SWWAF_LOOKUP_DB_PATH` naming a nothing. `SWWAF_LOOKUP_SOURCE=file` looks addresses up in a database file on
database file on your own host, looks addresses up in that file (see "Lookup of your own host (see "Lookup of AS number and country" in [`SPEC.md`](SPEC.md)).
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
@@ -150,10 +149,9 @@ 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. A request one step refuses `smallwebwaf` handles each request in this order. The first step that refuses it
skips the steps after it, apart from step 13. Nothing of a refused request ends the handling. Nothing of a refused request reaches the app. "What happens
reaches the app. "What happens to a request" in [`SPEC.md`](SPEC.md) gives each to a request" in [`SPEC.md`](SPEC.md) gives each step in full.
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.
@@ -189,18 +187,18 @@ 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, once 11. The Core Rule Set inspects the request's method, URL and headers, and the
`SWWAF_WAF_BODY_LIMIT` is set, form, JSON and XML bodies. A request it takes body too once `SWWAF_WAF_BODY_LIMIT` is set. A request it takes for an
for an attack is refused while `SWWAF_WAF_MODE` is `block`, the default, and attack is refused while `SWWAF_WAF_MODE` is `block`, the default, and let
let through while it is `detect`. It bans no one by itself. 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, a request passed to the app has its bytes counted 13. Once the answer has ended, the request's bytes are counted for the byte
for the byte limits. If a rule, a trap path, the Core Rule Set or a token limits. If a rule, a trap path, the Core Rule Set or a token check refused
check refused the request, the refusal is counted for the error burst. The it, the refusal is counted for the error burst. The client's history and the
client's history and the anomaly counters are updated. More than anomaly counters are updated. More than `SWWAF_ERROR_BURST_THRESHOLD`
`SWWAF_ERROR_BURST_THRESHOLD` refusals of one client in a minute ban it. 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
@@ -211,32 +209,27 @@ 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, so that traefik be `429` instead, or `close`, which closes the connection without an answer.
answers `502`. It answers: 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.
- For a size or time limit: - `413` or `408` when the client passes a size or time limit, `502` or `504`
- `431` for headers over their limit; when the app does.
- `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 you trust, such as your monitoring. Their - `SWWAF_ALLOW_NETS`: netblocks whose clients are refused only for a size or
clients are refused only for a size or time limit or a missing or wrong token. time limit or a missing or wrong token, such as your monitoring.
- `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
@@ -244,8 +237,7 @@ 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. Keep the six default ids in a list you give (see matched.
[`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.
@@ -269,8 +261,9 @@ 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` (see "Bans" in makes the ban permanent. Lift the ban by editing `bans.json` or through the
[`SPEC.md`](SPEC.md)). ban endpoint (see "Bans" in [`SPEC.md`](SPEC.md) and
[`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.
@@ -292,8 +285,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 on the host what the other scripts need, with - `script/bootstrap`: installs what the other scripts need on the host.
`make bootstrap`: `make bootstrap`. They need:
- `make`, `git` and `curl`; - `make`, `git` and `curl`;
- Go, for `gofmt`; - Go, for `gofmt`;
- node, yarn and prettier. - node, yarn and prettier.
@@ -423,9 +416,9 @@ another port, the health check's timing, and the forwarded headers.
## TODO ## TODO
- Planned, as "Planned, not built" in [`SPEC.md`](SPEC.md) describes: - Everything `SPEC.md` describes is built.
- sharing bans between the instances of several apps; - Two points `SPEC.md` defers until there is experience to go on: sharing bans
- sending the log over RELP. between the instances of several apps, and sending the log over RELP.
## Documents ## Documents
+5 -8
View File
@@ -78,14 +78,11 @@ 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. Empty for - `client`: the address whose request raised the alert. `netblock`: the ban's
`source_failure` and `file_error`. netblock, the netblock an anomaly counted, or, for the other events, the
- `netblock`: empty for `source_failure` and `file_error`; otherwise one of: client as rate limiting counts it: an IPv4 address as a /32, or an IPv6 group.
- the ban's netblock; Both are empty for `source_failure` and `file_error`.
- the netblock an anomaly counted; - `asn`, `as_name` and `country`: the client's as known when the alert was
- 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`.
+34 -38
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 ("Size and time limits" in `SPEC.md`). A gitea that takes seconds is cut off, and no one is banned for it. "Size and time limits" in
large uploads needs `SWWAF_REQUEST_MAX_BYTES`, `SPEC.md` gives the answers the client gets. A gitea that takes large
`SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_UPSTREAM_REQUEST_TIMEOUT` raised uploads needs `SWWAF_REQUEST_MAX_BYTES`, `SWWAF_CLIENT_REQUEST_TIMEOUT` and
to fit. `SWWAF_UPSTREAM_REQUEST_TIMEOUT` raised 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. A search term is a query parameter, which the Core Most often this is a search. Every query parameter is checked against the Core
Rule Set checks against its lists of system files and commands (see the fourth Rule Set's lists of system files and commands, except the ten named under
change under "Attack detection" in `SPEC.md`). A release attachment uploaded "Attack detection" in `SPEC.md`. A release attachment uploaded through the API
through the API as `docker-compose.yml` is refused the same way. Examples: 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, such as `python3` or `ssh key`, or has - text that starts with a command name, or whose first word has one right after
one right after a `/` in its first word, such as `feat/docker-support`; a `/`, such as `python3`, `ssh key` or `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 @@ through the API 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 query parameter holding a branch, `python3`, `ansible`, `base64` and `whoami`. A branch, tag or file name that
tag or file name that starts with one is refused. So is one that has such a name starts with one is refused. So is one that has such a name right after a `/`,
right after a `/`, such as `feat/docker-support`, `fix/docker-build` or 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. A listing sends it when - `refSubUrl`, the branch or tag of a directory listing. It is sent when the
it asks for its entries' last commits in a second request. It does so whenever listing asks for its entries' last commits in a second request, which it does
gitea takes more than a second to work them out; whenever 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 too in the ten query parameters that look for in some file names. This holds in the parameters `smallwebwaf` leaves
930120, 932160 and 932260 do not check (see the fourth change under "Attack out of 930120, 932160 and 932260 too (see the changes to the Core Rule Set under
detection" in `SPEC.md`). Examples: "Attack 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, for a visitor who opens a page of the file that needs - the sign-in page gitea sends a visitor to on the way to a page of the file
signing in, such as its edit page (`redirect_to`). The visitor can still sign that needs signing in, such as its edit page (`redirect_to`). The visitor can
in by opening the sign-in page itself. still sign in by opening the sign-in page itself.
### A path ### A path
@@ -143,30 +143,25 @@ 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, these are refused: For such a file, the page of the file, its raw file, its history, its blame and
its edit page are refused. The directory that holds it lists it, and a clone
- the page of the file; carries it.
- 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. It is refused too when the owner starts with an entry on the list of commands. So is one whose name after
image's name after the owner starts with one. Examples are `owner/python3-app`, the owner does. Examples are `owner/python3-app`, `owner/docker-base`, and any
`owner/docker-base`, and any image of an owner named `ansible` or `docker-team`. 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 ("Attack detection" in `SPEC.md`). toward the error burst.
### Artifact uploads from `actions/upload-artifact@v3` ### Artifact uploads from `actions/upload-artifact@v3`
@@ -174,9 +169,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. The error burst ("Attack detection" in `SPEC.md`) bans it after more the runner. More than 30 such refusals from its address within a minute, as when
than 30 such refusals from its address in a minute, as when many jobs upload at many jobs upload at once, ban it through the error burst.
once. `actions/upload-artifact@v4` does not send the header. `actions/upload-artifact@v4` does not send the header.
## What body inspection refuses ## What body inspection refuses
@@ -198,7 +193,8 @@ 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 for a visitor who opens the edit page of `document.write.js`; - the sign-in page gitea sends a visitor to on the way to the edit page of
`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`;
+2 -3
View File
@@ -31,7 +31,6 @@ 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 its last commit - `runsvinit`: the archived repository, unchanged since 2015, at tag `v2.0.0`,
(`b4b2c78`, eight commits after `v2.0.0`), by commit hash, with a `go.mod` the by commit hash, with a `go.mod` the build writes.
build writes.
- The Go image that builds `smallwebwaf`: by digest. - The Go image that builds `smallwebwaf`: by digest.
+5 -3
View File
@@ -1,8 +1,10 @@
# The metrics # The metrics
The metrics `GET /_smallwebwaf/metrics` answers with, as "Metrics endpoint" in `GET /_smallwebwaf/metrics` answers in the Prometheus text format. The
[`SPEC.md`](../SPEC.md) describes it. Prometheus keeps the `instance` label as requirements are under "Metrics endpoint" in [`SPEC.md`](../SPEC.md); this file
`exported_instance` unless the scrape sets `honor_labels: true`. lists the metrics. Every metric carries the label `instance`, set to
`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
+12 -23
View File
@@ -12,13 +12,9 @@ sent to a syslog server. A field that does not apply to a request is left out.
## Every line ## Every line
Every line has: Every line has `type` (`request`; the process's own messages have `process`),
the standard web log fields, `request_id`, `peer_ip`, `client_group`, `asn`,
- `type`: `request` (the process's own messages have `process`); `as_name`, `country`, `action` and `duration_total`.
- 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
@@ -32,7 +28,7 @@ Every line has:
## 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. 26 letters and digits. It goes to the app in `X-Request-ID`.
- `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
@@ -77,22 +73,15 @@ Every line has:
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. It included. It is left out when rate limiting did not count the request. A count
holds: can have a fraction, since each window is counted in two buckets with the
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;
A count can have a fraction (see "Counting method" in `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; or
[`SPEC.md`](../SPEC.md)). `error_burst`. A request whose bytes broke a limit was passed on whole, so its
`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
@@ -102,7 +91,7 @@ Every line has:
- the CrowdSec decision list's URL; - the CrowdSec decision list's URL;
- DNS blocklist zones; - DNS blocklist zones;
- `abuseipdb`. - `abuseipdb`.
- `offence`: `limit`, for a request that broke a limit. - `offence`: the kind of offence recorded, when one was.
- `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`.
+18 -17
View File
@@ -2,7 +2,8 @@
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. that take `off`, the `_FILE` form, and what each group does and why. A setting
set to a value its entry does not allow stops the start.
## Forms of values ## Forms of values
@@ -26,9 +27,8 @@ that take `off`, the `_FILE` form, and what each group does and why.
- `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: `:` and a - `SWWAF_LISTEN_ADDR` (default `:8080`): where `smallwebwaf` listens, an
port, such as `:9000`. Leave the host out (see "Ports" in optional host and a port. The host stays empty.
[`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 @@ that take `off`, the `_FILE` form, and what each group does and why.
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`): how many of its own bans `smallwebwaf` - `SWWAF_MAX_BANS` (default `5000`): the most bans `smallwebwaf` made that are
keeps, a whole number above zero. kept, a whole number above zero.
## Tokens ## Tokens
@@ -90,9 +90,8 @@ that take `off`, the `_FILE` form, and what each group does and why.
## Static lists ## Static lists
- `SWWAF_ALLOW_NETS` (default empty): netblocks you trust, such as your - `SWWAF_ALLOW_NETS` (default empty): netblocks whose clients are refused only
monitoring. Their clients are refused only for a size or time limit or a for a size or time limit or a missing or wrong token, such as your monitoring.
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.
@@ -151,7 +150,7 @@ that take `off`, the `_FILE` form, and what each group does and why.
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
stops the start. is refused.
## Biased thresholds ## Biased thresholds
@@ -165,8 +164,8 @@ that take `off`, the `_FILE` form, and what each group does and why.
- `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 with one AS number and percentage on each line, such as text file of `AS<number>:<percent>` lines, one to a line. A URL that is also
`AS64496:50`. A URL that is also in `SWWAF_BLOCKLIST_URLS` is refused. in `SWWAF_BLOCKLIST_URLS` is refused.
## Attack detection ## Attack detection
@@ -261,8 +260,8 @@ that take `off`, the `_FILE` form, and what each group does and why.
`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 after an alert is sent its - `SWWAF_ALERT_COOLDOWN` (default `15m`): how long a repeat of an alert is held
repeats are held back. A duration or `off`. 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`.
@@ -295,8 +294,10 @@ as `REQUESTS_PER_MINUTE`:
## Settings given as files ## Settings given as files
To give a token as a file, as "Tokens" under "Deployment" in Add `_FILE` to a setting's name to read its value from a file:
[`SPEC.md`](../SPEC.md) describes, make the file on the host: `SWWAF_METRICS_TOKEN_FILE` names the file that holds `SWWAF_METRICS_TOKEN`. To
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
@@ -305,7 +306,7 @@ chown 65532:65532 /srv/app/tokens/metrics
chmod 0400 /srv/app/tokens/metrics chmod 0400 /srv/app/tokens/metrics
``` ```
Then mount its directory into the container read-only, and name the file: Mount its directory into the container read-only, and name the file:
```sh ```sh
docker run \ docker run \
+15 -19
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.
An entry an admin adds needs `netblock`, `start` and `expires`; `reason`, To ban a netblock, add an entry with its `netblock`, `start` and `expires`.
`notes` and `cause` may be left out, and the cause is then `admin`: `reason`, `notes` and `cause` may be left out; the cause is then `admin`:
```json ```json
{ {
@@ -32,23 +32,20 @@ An entry an admin adds needs `netblock`, `start` and `expires`; `reason`,
} }
``` ```
"Bans" in [`SPEC.md`](../SPEC.md) says how an edit lifts, keeps or forgets a To lift a ban, add `"lifted"` with the time you lift it, such as
ban. The notes: `"lifted": "2026-10-06T13:00:00Z"`. To keep a ban `smallwebwaf` made, set its
`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);
- for a broken limit, `kind` (`requests`, `bytes` or `refusals`), `limit`, - what was broken: the rule id and target that matched, or the trap path, or the
`window` and `count`: what the limit was on, the limit, its window and the limit, its window and the count reached;
count reached; - the client's percentage of the limit, with the setting that gave it;
- `limit_percent` and `limit_percent_setting`: the client's percentage of that - the reputation sources the client is on;
limit and the setting that gave it, when a setting lowered it; - `request`: the request that caused the ban: time, method, host, path with its
- for a clear sign of attack, `rule_id` and `target`, or `trap_path`; query string, status and user agent, each text cut to 256 bytes;
- `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.
@@ -96,9 +93,8 @@ 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`. Written every 15 minutes, a full file comes to the default `SWWAF_MAX_BANS`. With a full file, a hundred new bans a day come
about 600 MiB of writes a day, and a hundred new bans a day add about 600 MiB to about 600 MiB of writes.
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.