Seventh review: false statements fixed, README shortened to link the spec
check / check (push) Canceled after 0s

- Engine error handling dropped as not built; unknown country and AS
  number each on its own; observe mode makes no ban of its own.
- README no longer restates the request steps, the refusal answers or
  the design; its step list uses the spec's numbers.
- Repeats, misreadable sentences, late verbs and long sentences reworded
  as the review asks.

Model: fable-5-1
This commit is contained in:
2026-10-26 00:37:38 +00:00
parent a62909dbf4
commit 8282404453
3 changed files with 114 additions and 184 deletions
+58 -54
View File
@@ -164,8 +164,8 @@ its rules.
thresholds are evaluated.
In `SWWAF_MODE=observe`, every refusal above is logged as what would have
happened, nothing is refused, and no ban is made. The size and time limits and
the token check still apply.
happened, nothing is refused, and `smallwebwaf` makes no ban of its own. The
size and time limits and the token check still apply.
## Counting method
@@ -183,8 +183,8 @@ the token check still apply.
GeoJS answers, the reputation verdicts and scores, the DNS queries under way
and the anomaly counters have fixed bounds, listed under "Fixed limits" in
`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
that bound, a zone is asked at the client's next request.
longest ago goes first, and the verdict or score fetched longest ago. Past the
bound on DNS queries under way, a zone is asked at the client's next request.
## Bans
@@ -215,9 +215,12 @@ to a ban, each by its own rule:
permanent this way.
- Once that ban has run out, the netblock is served like any other. Its next
clear sign of attack bans it permanently at once.
- A broken limit: a request over a rate limit, a response that takes the
client's byte total past a byte limit, or more than
`SWWAF_ERROR_BURST_THRESHOLD` refusals within a minute.
- A broken limit, one of:
- a request over a rate limit;
- a response that takes the client's byte total past a byte limit;
- more than `SWWAF_ERROR_BURST_THRESHOLD` refusals within a minute.
The bans for a broken limit:
- The first such ban lasts `SWWAF_LIMIT_BAN_DURATION` (default `1h`), with
the cause `limit`. So does one that comes more than
`SWWAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`) after the last such ban
@@ -233,7 +236,7 @@ to a ban, each by its own rule:
A Core Rule Set match refuses only the request it matched, with `403` and
without a ban, because the Core Rule Set has false positives. A `block` rule
does the same. Those refusals count toward the error burst, so a client that
keeps setting them off is banned under the second rule.
keeps setting them off is banned for a broken limit.
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
@@ -328,8 +331,8 @@ Conventions:
- `SWWAF_ADMIN_TOKEN` switches on the ban endpoints and
`/_smallwebwaf/clients/<ip>`. The metrics are off until `SWWAF_METRICS_TOKEN`
is set, since without a token they would be open to anyone on the internet.
The two are separate, so a scraper that holds the metrics token cannot manage
bans.
The two are separate, so the metrics collector, which holds the metrics token,
cannot manage bans.
- A token is a long random value, since the endpoints can be reached from the
internet. One shorter than 32 characters stops the start. The app starts with
the same environment variables as `smallwebwaf`, so it can read a token set as
@@ -453,8 +456,8 @@ service that keeps its visitors' addresses on its own host.
Addresses are asked about together, up to 200 in one request
(`https://get.geojs.io/v1/ip/geo.json?ip=a,b,c`), one request at a time, which
keeps the number of requests to GeoJS low. Each answer's `country_code`, `asn`
and `organization_name` are read. An answer without a `country_code`, or with
the `asn` `64512`, counts as unknown.
and `organization_name` are read. An answer without a `country_code` leaves
the country unknown, and the `asn` `64512` leaves the AS number unknown.
- Each answer is kept for seven days, in memory and in `lookups.json`. Answers
are kept apart from the table of clients, so that an answer outlasts a client
dropped from that table. The answer reaches the client's history and ban notes
@@ -528,12 +531,12 @@ service's abuse comes from.
fetched and kept like a blocklist (see "Reputation"). Where it and
`SWWAF_ASN_LIMIT_PERCENT` both give a percentage, the lower applies.
- A client's limit percentage is the lowest of those that apply to it: its AS
number's, its country's, or a reputation hit's `limit:<percent>`. A client the
lookup cannot place gets `SWWAF_UNKNOWN_LIMIT_PERCENT` in place of an AS
number's or a country's. The percentage is 100 if none applies. Clients given
a lower percentage are banned after fewer requests, first for a while and
then, for repeated abuse, permanently. The log line of each counted request,
and the notes of each ban, say which setting lowered the limit.
number's, its country's, or a reputation hit's `limit:<percent>`. A client
without a country gets `SWWAF_UNKNOWN_LIMIT_PERCENT` in place of a country's.
The percentage is 100 if none applies. Clients given a lower percentage are
banned after fewer requests, first for a while and then, for repeated abuse,
permanently. The log line of each counted request, and the notes of each ban,
say which setting lowered the limit.
- No budget is shared by a whole country or AS number. One abuser could use it
up and lock out everyone else there: a denial of service nobody chose, and the
one this tool exists to prevent. Refusing a whole country is left to the
@@ -544,15 +547,14 @@ service's abuse comes from.
By default the Core Rule Set reads the method, the URL with its query string,
and the headers of every request. That is where automated attacks show:
injection in query strings, path traversal, attacks carried in headers,
scanners' user agents. It reads no request body. On a code forge, the example
deployment, bodies carry what people write and publish, such as issue text and
scanners' user agents. It reads no request body. In front of a code forge such
as gitea, bodies carry what people write and publish, such as issue text and
files saved in the web editor. The Core Rule Set takes the shell commands and
code in them for attacks. The default gives up refusing an attack carried in a
body. The rule files, the limits and the bans still apply to the client that
sends it.
- The Core Rule Set skips a path under `SWWAF_WAF_EXEMPT_PATHS`. An engine error
on a request forwards the request, and is logged and counted.
- The Core Rule Set skips a path under `SWWAF_WAF_EXEMPT_PATHS`.
- `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
rules, which find more attacks and take more ordinary requests for attacks.
@@ -633,12 +635,12 @@ every app, and no setting undoes them:
- `ref`, `sha`, `branch` and `workflow`;
- `artifactName` and `redirect_to`.
gitea puts file paths, branch and workflow names, and the page to return to
after signing in into them. It reads these values as names in a repository
gitea puts into them file paths, branch and workflow names, and the page to
return to after signing in. It reads these values as names in a repository
or in its own records, or as a page of its own site, never as a command. Any
name on those lists can be an ordinary file or branch, such as `.gitignore`
or `docker-build`. In these parameters, anything only these three rules
catch reaches the app, commands such as `|cat /etc/passwd` included. Path
or `docker-build`. In these parameters, what only these three rules would
catch reaches the app, such as the command `|cat /etc/passwd`. Path
traversal, SQL and script injection, and PHP, Java and Node.js code are
still refused there. Every other parameter keeps all three rules. An app
that uses one of these parameters as a file on the server, or passes it to a
@@ -647,8 +649,8 @@ every app, and no setting undoes them:
5. Two cookies are left out, and `Referer` keeps fewer rules:
- The Core Rule Set reads the request without the `gitea_flash` and
`redirect_to` cookies. gitea puts a message naming what was just done into
`gitea_flash`. It puts the page to return to after signing in into
`redirect_to`. The Core Rule Set takes file names in them, such as
`gitea_flash`. It puts into `redirect_to` the page to return to after
signing in. The Core Rule Set takes file names in them, such as
`package.json`, for attacks. A browser sends such a cookie with every
request until gitea replaces it. gitea cannot replace it while those
requests are refused, so one refusal would keep the browser out of the
@@ -772,7 +774,8 @@ too close together.
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
in `reputation.json`. A source that is down or over quota gives no verdict.
The service continues, with one `source_failure` alert per cooldown.
The service continues. Each failure raises a `source_failure` alert (see
"Alerting").
### Alerting
@@ -861,11 +864,11 @@ and the default file.
wrong for every app, with its path rules anchored at the site root. In front
of gitea, `/.env` is a probe, while
`/<owner>/<repo>/src/branch/main/.env.example` is a file in a repository that
any visitor may open. `ban` rules of your own follow the same rule. Anything
app-specific, such as a rule banning WordPress probes in front of gitea,
belongs in a file the app's Dockerfile adds or the deployer mounts. A
directory mounted over `SWWAF_RULES_DIR` replaces the default file; single
files mounted into it add to it.
any visitor may open. Anchor the paths of your own `ban` rules at the site
root too. Anything app-specific, such as a rule banning WordPress probes in
front of gitea, belongs in a file the app's Dockerfile adds or the deployer
mounts. A 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
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
@@ -874,11 +877,11 @@ and the default file.
## Persistent state
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 remote log is the exception; see
"Request log"). The files are meant for people as well: an admin can read or
edit them at any time, and the running `smallwebwaf` takes the edit in.
`docs/state-files.md` lists the files, what each holds, and their sizes.
All state lives in memory, and the files in `SWWAF_STATE_DIR` hold a copy of it,
so an orderly stop loses nothing. The files are meant for people as well: an
admin can read or edit them at any time, and the running `smallwebwaf` takes the
edit in. `docs/state-files.md` lists the files, what each holds, and their
sizes.
- Each file holds one kind of state, as JSON with:
- a top-level `version` number;
@@ -1019,14 +1022,14 @@ what the image holds and how each part is pinned.
digest.
- nixpkgs comes from a release file on `releases.nixos.org`, named with its
hash, not from a GitHub archive, whose bytes can change. Root uses Nix
directly; no Nix daemon runs, and since `nix-bin` creates no build users,
`build-users-group =` in `/etc/nix/nix.conf` lets root build without them.
Root's Nix profile is last on the `PATH`, so that no package hides the image's
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
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
release, every six months.
directly; no Nix daemon runs. `nix-bin` creates no build users, so
`/etc/nix/nix.conf` sets `build-users-group =`, which lets root build without
them. Root's Nix profile is last on the `PATH`, so that no package hides the
image's 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 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 release, every six months.
- runit supervises the two processes, with `runsvinit` as the entrypoint, as the
owner's code style guide asks for service containers. No package carries
`runsvinit`, so the image builds it from its archived source, unchanged since
@@ -1075,10 +1078,10 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
- an unwritable `SWWAF_STATE_DIR`;
- a `SWWAF_RULES_DIR` that does not exist or cannot be read;
- a `SWWAF_LISTEN_ADDR` it cannot listen on.
- A `smallwebwaf` that stops at start is therefore started again every second,
logging its message each time, until the cause is fixed; the health check
fails meanwhile. Once running, a failing GeoJS, reputation source or alert
destination, or a broken edit, never takes the protected service down.
- A `smallwebwaf` that stops at start logs its message at each try until the
cause is fixed; the health check fails meanwhile. Once running, a failing
GeoJS, reputation source or alert destination, or a broken edit, never takes
the protected service down.
- `docker stop` sends `SIGTERM` to `runsvinit`, which has runit stop both
services at once, each with `SIGTERM`, and exits once they have stopped.
`smallwebwaf` stops taking requests, gives those in progress five seconds to
@@ -1147,9 +1150,10 @@ all: `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32,::1/128`.
### upaas
An app deployed by upaas takes this shape with no change to upaas. upaas builds
the app's Dockerfile and runs the one container as it runs any app. The traefik
labels route to port 8080, and any `SWWAF_` settings go with the app's
An app deployed by upaas, which rebuilds and restarts an app's Docker container
when its repository changes, takes this shape with no change to upaas. upaas
builds the app's Dockerfile and runs the one container as it runs any app. The
traefik labels route to port 8080, and any `SWWAF_` settings go with the app's
environment variables. The volume for `/var/lib/smallwebwaf` and the directory
of any token file go beside the app's own volumes. gitea is built on the image
like any other app; `docs/gitea.md` records what the defaults do in front of it,