Seventh review: false statements fixed, README shortened to link the spec
check / check (push) Canceled after 0s
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user