2 Commits
Author SHA1 Message Date
sneak 9af927c8cf README reworked after review: every finding fixed, every sentence reread
check / check (push) Canceled after 0s
The eighteen findings of the review on the PR are fixed: statements the
code does not bear out, the stacked "a client a blocklist lists" form,
late verbs, sentences over thirty words, comma chains where bullets
belong, sentences a newcomer reads twice, and one project-plan remark.
Every other sentence was reread against the same rules, and the ones
that still put the object before the subject were rewritten.

Model: fable-5-1
2026-10-22 19:13:56 +00:00
sneak 93ce3b210c 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 a narrative of milestones and one sentence
listing every feature, and gave its settings as one list of a hundred
entries. The new one leads with what smallwebwaf is, what it protects
against, how to build and deploy it, what happens to a request, what a
refused visitor and the operator see, and how to check it works. The
settings follow, grouped by topic, each with its default and what a bad
value does. The rest is reference: bans, rule files, the Core Rule Set,
the admin endpoints, the request log, alerts, metrics, state files and
the reputation sources. Every statement was checked against the code on
next; nothing describes unbuilt behaviour. Short sentences, bulleted
lists in place of comma chains, no tables, every identifier in
backticks, no project history.

Model: fable-5-1
2026-10-22 18:37:56 +00:00
+85 -128
View File
@@ -211,11 +211,9 @@ ends the handling. Nothing of a refused request reaches the app.
Two terms recur below. A clear sign of attack is a request that matches a rule Two terms recur below. A clear sign of attack is a request that matches a rule
whose action is `ban`, or asks for a trap path (steps 9 and 10). An offence is a whose action is `ban`, or asks for a trap path (steps 9 and 10). An offence is a
request that counts against its client in its history. It is one of: request that counts against its client in the client's history: a clear sign of
attack, a broken limit, or a refusal by a rule, by the Core Rule Set or for a
- a clear sign of attack; missing or wrong token.
- a broken limit;
- a refusal by a rule, by the Core Rule Set or for a missing or wrong token.
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.
@@ -355,8 +353,8 @@ configuration before enforcing it.
`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 for good. Lift the ban by editing `bans.json` or through the ban endpoint (see
ban endpoint (see "Bans" below). "Bans" below).
- 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 (see "Metrics" below). answers with the metrics (see "Metrics" below).
@@ -419,8 +417,8 @@ The forms of values:
`SWWAF_ALERT_NTFY_URL` is set, it may hold no control character. While `SWWAF_ALERT_NTFY_URL` is set, it may hold no control character. While
`SWWAF_LOG_REMOTE_URL` is set and `SWWAF_LOG_REMOTE_APP_NAME` is not, it must `SWWAF_LOG_REMOTE_URL` is set and `SWWAF_LOG_REMOTE_APP_NAME` is not, it must
be 1 to 48 printable ASCII characters without a space. be 1 to 48 printable ASCII characters without a space.
- `SWWAF_MODE` (default `enforce`): `enforce`, or `observe` to ban no one and - `SWWAF_MODE` (default `enforce`): `enforce`, or `observe` to refuse nothing
log what `enforce` would have done (see "What happens to a request" above). and log what would have been refused (see "What happens to a request" above).
- `SWWAF_TRUSTED_PROXIES` (default `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`, - `SWWAF_TRUSTED_PROXIES` (default `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`,
the private address ranges): the netblocks whose `X-Forwarded-For` is the private address ranges): the netblocks whose `X-Forwarded-For` is
believed, normally where traefik comes from. Set but empty, it trusts nothing: believed, normally where traefik comes from. Set but empty, it trusts nothing:
@@ -552,8 +550,8 @@ ban.
Clients of the AS numbers and countries listed here get a percentage of every Clients of the AS numbers and countries listed here get a percentage of every
rate limit and byte limit. The same rules then ban them after fewer requests. A rate limit and byte limit. The same rules then ban them after fewer requests. A
lowered limit is rounded down. `0` is a zero allowance: the client's first lowered limit is rounded down. `0` is a zero allowance: the client's first
request bans it. Each client is counted on its own. An AS number or a country request bans it. Each client is counted on its own. No budget is shared by a
has no shared budget, since one abuser could use it up for everyone in it. whole AS number or country, which one abuser could use up.
- `SWWAF_ASN_LIMIT_PERCENT` (default empty): a list of AS numbers, each with `:` - `SWWAF_ASN_LIMIT_PERCENT` (default empty): a list of AS numbers, each with `:`
and a percentage, such as `AS14061:50,AS16276:50,AS45102:25`. An AS number and a percentage, such as `AS14061:50,AS16276:50,AS45102:25`. An AS number
@@ -624,8 +622,8 @@ do. The defaults judge it by what it does to your service.
- `SWWAF_REPUTATION_ACTION` (default `limit:25`): what is done with a client on - `SWWAF_REPUTATION_ACTION` (default `limit:25`): what is done with a client on
a DNS blocklist, or one whose AbuseIPDB score is a hit. `deny`, a DNS blocklist, or one whose AbuseIPDB score is a hit. `deny`,
`limit:<percent>` or `log`, as for `SWWAF_BLOCKLIST_ACTION`. Such a verdict is `limit:<percent>` or `log`, as for `SWWAF_BLOCKLIST_ACTION`. Such a verdict is
less certain than the Spamhaus DROP list, so by default the client gets a less certain than a list like DROP, so by default the client gets a quarter of
quarter of every limit. every limit.
- `SWWAF_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or - `SWWAF_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict or
AbuseIPDB's score is used. A duration; it cannot be `off`. AbuseIPDB's score is used. A duration; it cannot be `off`.
- `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a - `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone or a
@@ -668,11 +666,10 @@ See "Blocklists", "DNS blocklists", "AbuseIPDB" and "CrowdSec" below.
`.git/`, `.gitignore` or `package.json`. `.git/`, `.gitignore` or `package.json`.
In front of a code forge these refuse git over HTTP, uploads and views of In front of a code forge these refuse git over HTTP, uploads and views of
ordinary files. The default rule file still bans probes for version control ordinary files. The default rule file bans those probes at the site root
directories and secret files at the site root. A list given replaces the instead. A list given replaces the default, so include them. An id must be a
default, so include them. An id must be a whole number above zero. An id whole number above zero. An id from 900000 to 900999 is refused: those are
from 900000 to 900999 is refused: those are the rules that set the Core Rule the rules that set the Core Rule Set up, and `smallwebwaf`'s own.
Set up, and `smallwebwaf`'s own.
- `SWWAF_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does - `SWWAF_WAF_EXEMPT_PATHS` (default empty): path prefixes the Core Rule Set does
not inspect, matched as `SWWAF_RATE_LIMIT_EXEMPT_PATHS` is. not inspect, matched as `SWWAF_RATE_LIMIT_EXEMPT_PATHS` is.
@@ -797,8 +794,7 @@ thresholds per client are:
- `SWWAF_ANOMALY_CLIENT_BYTES_PER_MINUTE`; - `SWWAF_ANOMALY_CLIENT_BYTES_PER_MINUTE`;
- `SWWAF_ANOMALY_CLIENT_BYTES_PER_HOUR`. - `SWWAF_ANOMALY_CLIENT_BYTES_PER_HOUR`.
The other scopes' names have another prefix before the same four endings, such The other scopes have the same four names with `CLIENT` replaced:
as `REQUESTS_PER_MINUTE`:
- `SWWAF_ANOMALY_NET_...`: per netblock around a client. Two settings give the - `SWWAF_ANOMALY_NET_...`: per netblock around a client. Two settings give the
netblock's length: netblock's length:
@@ -847,8 +843,6 @@ docker run \
... ...
``` ```
### Fixed limits
A few limits are fixed rather than settings: A few limits are fixed rather than settings:
- at most 100,000 GeoJS answers are kept, for 7 days each; - at most 100,000 GeoJS answers are kept, for 7 days each;
@@ -859,13 +853,9 @@ A few limits are fixed rather than settings:
## Bans ## Bans
A ban refuses every request from a netblock until it ends, with A ban refuses every request from a netblock until it ends, with
`SWWAF_BAN_RESPONSE`. The netblock is one of: `SWWAF_BAN_RESPONSE`. The netblock is the client's IPv4 address, or the netblock
around it, `SWWAF_BAN_SCOPE_V4_PREFIX` long, or the client's IPv6 group. A
- the client's IPv4 address; banned client is not looked up and not counted by rate limiting.
- the netblock around it, `SWWAF_BAN_SCOPE_V4_PREFIX` long;
- the client's IPv6 group.
A banned client is not looked up and not counted by rate limiting.
`smallwebwaf` makes bans for three causes: `smallwebwaf` makes bans for three causes:
@@ -901,8 +891,8 @@ Each ban carries notes for deciding whether to lift it:
refused; refused;
- how many earlier bans the netblock had, by cause. - how many earlier bans the netblock had, by cause.
`smallwebwaf` keeps at most `SWWAF_MAX_BANS` of the bans it made. Past that, one At most `SWWAF_MAX_BANS` bans `smallwebwaf` made are kept. Past that, one ban is
ban is dropped: the earliest ban of the netblock that has gone longest without a dropped: the earliest ban of the netblock that has gone longest without a
request. Bans whose cause is `admin` are never dropped. Those are the bans you request. Bans whose cause is `admin` are never dropped. Those are the bans you
make, and the bans `smallwebwaf` made that you keep by setting their cause. make, and the bans `smallwebwaf` made that you keep by setting their cause.
@@ -1006,17 +996,13 @@ scanner-agent user_agent ban (?i)\b(sqlmap|nikto|nuclei|wpscan)\b
takes time linear in the text it reads. It matches anywhere in the target takes time linear in the text it reads. It matches anywhere in the target
unless anchored with `^` and `$`. `(?i)` at its front makes it ignore case. unless anchored with `^` and `$`. `(?i)` at its front makes it ignore case.
These stop the start, with a message naming the file and line: A line that is not a rule stops the start, with a message naming the file and
line. So does a bad `header:` name, a rule for `Host` or `Transfer-Encoding`, a
- a line that is not a rule; regex that does not compile, or an id used twice. While it runs, `smallwebwaf`
- a bad `header:` name, or a rule for `Host` or `Transfer-Encoding`; watches the directory. It reads the files again once the directory has had no
- a regex that does not compile; change for 2 seconds, so that a file still being written is read whole. If the
- an id used twice. files then hold such an error, the rules stay as they were. The log and a
`file_error` alert name the file and line.
While it runs, `smallwebwaf` watches the directory. It reads the files again
once the directory has had no change for 2 seconds, so that a file still being
written is read whole. If the files then hold such an error, the rules stay as
they were. The log and a `file_error` alert name the file and line.
The image ships one file, `share/rules.d/00-default.rules`, installed as The image ships one file, `share/rules.d/00-default.rules`, installed as
`/etc/smallwebwaf/rules.d/00-default.rules`. Its rules: `/etc/smallwebwaf/rules.d/00-default.rules`. Its rules:
@@ -1071,18 +1057,14 @@ setting undoes them.
address or `localhost` (rules 931100 and 934110). Git credential helpers ask address or `localhost` (rules 931100 and 934110). Git credential helpers ask
to be sent back to such a URL. to be sent back to such a URL.
- Ten query parameters are not checked against three lists: system files - Ten query parameters are not checked against three lists: system files
(930120), shell paths (932160) and command names (932260). They are: (930120), shell paths (932160) and command names (932260). They are `path`,
- `path`, `files`, `skip-to` and `sub_path`; `files`, `skip-to`, `sub_path`, `ref`, `sha`, `branch`, `workflow`,
- `ref`, `sha`, `branch` and `workflow`; `artifactName` and `redirect_to`. A file such as `.gitignore` or a branch such
- `artifactName` and `redirect_to`. as `docker-build` then gets through there. So does an attack only those three
rules catch, such as `|cat /etc/passwd`. Path traversal and SQL injection are
A file such as `.gitignore` or a branch such as `docker-build` then gets still refused there. Names are matched without regard to case. An app that
through there. So does an attack only those three rules catch, such as uses one of these parameters as a file on the server, or passes it to a shell,
`|cat /etc/passwd`. Path traversal and SQL injection are still refused has to check it itself.
there. Names are matched without regard to case. An app that uses one of
these parameters as a file on the server, or passes it to a shell, has to
check it itself.
- The cookies `gitea_flash` and `redirect_to` are not read. `Referer` is not - The cookies `gitea_flash` and `redirect_to` are not read. `Referer` is not
checked for a Unix command given without arguments (932340), or for Java checked for a Unix command given without arguments (932340), or for Java
starting a process (944110). starting a process (944110).
@@ -1140,13 +1122,9 @@ refused ones included:
{"type":"request","time":"2026-10-03T12:00:00.123Z","instance":"fsn1app1/gitea","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"response_content_type":"text/html; charset=utf-8","upstream_status":200,"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"duration_total":3.217,"duration_checks":0.041,"duration_upstream_connect":0.052,"duration_upstream_first_byte":2.874,"duration_upstream_total":3.104} {"type":"request","time":"2026-10-03T12:00:00.123Z","instance":"fsn1app1/gitea","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"response_content_type":"text/html; charset=utf-8","upstream_status":200,"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"duration_total":3.217,"duration_checks":0.041,"duration_upstream_connect":0.052,"duration_upstream_first_byte":2.874,"duration_upstream_total":3.104}
``` ```
A field that does not apply to a request is left out. Every line has: A field that does not apply to a request is left out. Every line has `type`, the
fields from `time` to `user_agent`, `request_id`, `peer_ip`, `client_group`,
- `type`; `asn`, `as_name`, `country`, `action` and `duration_total`.
- the fields from `time` to `user_agent`;
- `request_id`, `peer_ip` and `client_group`;
- `asn`, `as_name` and `country`;
- `action` and `duration_total`.
- `time` is when the request arrived, in UTC. `instance` is - `time` is when the request arrived, in UTC. `instance` is
`SWWAF_INSTANCE_NAME`. `scheme` is the `X-Forwarded-Proto` a trusted proxy `SWWAF_INSTANCE_NAME`. `scheme` is the `X-Forwarded-Proto` a trusted proxy
@@ -1201,20 +1179,16 @@ A field that does not apply to a request is left out. Every line has:
limit; `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; or limit; `minute_bytes`, `hour_bytes` or `day_bytes` for a byte limit; or
`error_burst`. `offence` is then `limit`. A request whose bytes broke a limit `error_burst`. `offence` is then `limit`. A request whose bytes broke a limit
was passed on whole, so its `action` stays `forward`. was passed on whole, so its `action` stays `forward`.
- `reputation` lists the sources the client is on, in this order: - `reputation` lists the sources the client is on: blocklist URLs, the CrowdSec
- blocklist URLs; decision list's URL, DNS blocklist zones, then `abuseipdb`.
- the CrowdSec decision list's URL;
- DNS blocklist zones;
- `abuseipdb`.
- `ban_expires` is when the client's ban ends, for a request that made one or - `ban_expires` is when the client's ban ends, for a request that made one or
was refused under one: a time, or `permanent`. was refused under one: a time, or `permanent`.
- The timings are in milliseconds. `duration_total` runs from the request's - The timings are in milliseconds. `duration_total` runs from the request's
headers being read to its log line. `duration_checks` runs to the end of the headers being read to its log line. `duration_checks` runs to the end of the
checks, and `duration_waf` is the part of that the Core Rule Set took. The checks, and `duration_waf` is the part of that the Core Rule Set took. The
three `duration_upstream_` timings run from the hand-over to the app: three `duration_upstream_` timings run from the hand-over to the app:
- `connect`, until there was a connection; `connect` until there was a connection, `first_byte` until the first byte of
- `first_byte`, until the first byte of the answer; the answer, and `total` until the end.
- `total`, until the end.
No body is logged, and no header but those above. `smallwebwaf`'s own messages No body is logged, and no header but those above. `smallwebwaf`'s own messages
(start, the settings, stop, errors) share the stream as JSON lines marked (start, the settings, stop, errors) share the stream as JSON lines marked
@@ -1257,15 +1231,11 @@ its event is in `SWWAF_ALERT_EVENTS`:
- `reputation_hit`: a request from a client on a blocklist, the CrowdSec - `reputation_hit`: a request from a client on a blocklist, the CrowdSec
decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit. decision list or a DNS blocklist, or with an AbuseIPDB score that is a hit.
One alert per source. One alert per source.
- `source_failure`: one of: - `source_failure`: GeoJS failing, a list that cannot be fetched, a DNS
- GeoJS failing; blocklist or AbuseIPDB failing or refusing a query, or the day's AbuseIPDB
- a list that cannot be fetched; checks used up.
- a DNS blocklist or AbuseIPDB failing or refusing a query; - `file_error`: a rule file or state file edit with an error, a state file that
- the day's AbuseIPDB checks used up. cannot be written, or a replacement lookup database that cannot be read.
- `file_error`: one of:
- a rule file or state file edit with an error;
- a state file that cannot be written;
- a replacement lookup database that cannot be read.
The bans you make raise no alert. In `observe` mode, a request that would have The bans you make raise no alert. In `observe` mode, a request that would have
made a ban still raises the ban's alert, with `mode` set to `observe` in its made a ban still raises the ban's alert, with `mode` set to `observe` in its
@@ -1341,9 +1311,9 @@ line for each of these that the alert has:
- the file, the source, the error and the mode from `detail`; - the file, the source, the error and the mode from `detail`;
- the suppressed repeats, if any. - the suppressed repeats, if any.
Slack gets the title in bold and the text below it, with `&`, `<` and `>` Slack is posted the title in bold and the text below it, with `&`, `<` and `>`
escaped. ntfy gets the text, with the title in `Title`, the token if set, and a escaped. ntfy is posted the text, with the title in `Title`, the token if set,
priority and a tag by event: and a priority and a tag by event:
- `ban`: priority `default`, tag `no_entry`. - `ban`: priority `default`, tag `no_entry`.
- `permanent_ban`: priority `high`, tag `no_entry`. - `permanent_ban`: priority `high`, tag `no_entry`.
@@ -1357,9 +1327,8 @@ A repeat is an alert for the same event on the same netblock, source, file or
anomaly scope, less than `SWWAF_ALERT_COOLDOWN` after the last. It is held back anomaly scope, less than `SWWAF_ALERT_COOLDOWN` after the last. It is held back
and counted, and the next alert sent gives the count. Past and counted, and the next alert sent gives the count. Past
`SWWAF_ALERT_MAX_PER_HOUR` alerts in an hour, the rest are held back and counted `SWWAF_ALERT_MAX_PER_HOUR` alerts in an hour, the rest are held back and counted
by event. Once the hour ends, one alert with the event `summary` gives those by event. Once the hour ends, one alert with the event `summary` sums them up,
counts. It also gives the repeats a cooldown held back that no later alert with the repeats of the cooldowns that ran out.
reported.
Each destination has a queue of at most 1000 alerts, sent one at a time, so a Each destination has a queue of at most 1000 alerts, sent one at a time, so a
slow destination holds up nothing. A `2xx` answer takes an alert. A `4xx` other slow destination holds up nothing. A `2xx` answer takes an alert. A `4xx` other
@@ -1449,11 +1418,9 @@ keeps it as `exported_instance` unless the scrape sets `honor_labels: true`.
- its offences by kind: `limit`, `attack`, `rule_blocked`, `waf_blocked` and - its offences by kind: `limit`, `attack`, `rule_blocked`, `waf_blocked` and
`token_refused`. `token_refused`.
- `lookups.json`: GeoJS's answers, one per line. - `lookups.json`: GeoJS's answers, one per line.
- `reputation.json`: - `reputation.json`: the last good copy of each list fetched from a URL, with
- the last good copy of each list fetched from a URL, with when it was last when it was last tried; each DNS blocklist verdict still in use; and
tried; AbuseIPDB's scores and the checks spent today.
- each DNS blocklist verdict still in use;
- AbuseIPDB's scores, and the checks spent today.
- `alerts.json`: the alert cooldowns, the hour under way, the alerts waiting for - `alerts.json`: the alert cooldowns, the hour under way, the alerts waiting for
each destination, and the anomaly counters. each destination, and the anomaly counters.
@@ -1496,8 +1463,8 @@ AS number and country. These addresses are never sent:
- one under a ban; - one under a ban;
- a private, loopback or link-local address, which no source can place. - a private, loopback or link-local address, which no source can place.
For an IPv6 visitor, GeoJS is asked about the first address of its group. Each An IPv6 visitor is asked about by the first address of its group. Each answer is
answer is kept for seven days, in memory and in `lookups.json`. kept for seven days, in memory and in `lookups.json`.
A request waits for its client's first answer only when a setting needs the A request waits for its client's first answer only when a setting needs the
answer before the request goes on. It waits up to `SWWAF_LOOKUP_TIMEOUT`. The answer before the request goes on. It waits up to `SWWAF_LOOKUP_TIMEOUT`. The
@@ -1523,11 +1490,11 @@ address it does not hold counts as from an unknown country. `smallwebwaf` reads
the whole file at start. It reads the file again once it has gone 2 seconds the whole file at start. It reads the file again once it has gone 2 seconds
without a change after you replace it. A replacement it cannot read is logged without a change after you replace it. A replacement it cannot read is logged
and alerted, and the file read before stays in use. Mount the directory that and alerted, and the file read before stays in use. Mount the directory that
holds the file, not the file itself. Many editors and download tools save a file holds the file rather than the file itself: many editors and download tools
by replacing it, and a container keeps seeing the old file when only the file is replace a file when they save it, and a container keeps seeing the old one when
mounted. IPinfo releases the database under the Creative Commons the file alone is mounted. IPinfo releases the database under the Creative
Attribution-ShareAlike 4.0 licence. It asks for a link to IPinfo on the site Commons Attribution-ShareAlike 4.0 licence. It asks for a link to IPinfo on the
that uses it, such as "IP address data is powered by IPinfo". site that uses it, such as "IP address data is powered by IPinfo".
## Blocklists ## Blocklists
@@ -1548,8 +1515,8 @@ list again before its time. A fetch fails when:
`AS<number>:<percent>`. `AS<number>:<percent>`.
The copy fetched before stays in use. The failure is logged and raised as a The copy fetched before stays in use. The failure is logged and raised as a
`source_failure` alert. The log, the alerts, the ban notes and the metrics name `source_failure` alert. Each list is named by its URL in the log, the alerts,
each list by its URL, so keep a secret out of it. the ban notes and the metrics, so keep a secret out of it.
`deny` suits lists of networks that send nothing legitimate, such as DROP. `deny` suits lists of networks that send nothing legitimate, such as DROP.
`limit:<percent>` suits lists of addresses shared with ordinary visitors, such `limit:<percent>` suits lists of addresses shared with ordinary visitors, such
@@ -1596,23 +1563,18 @@ client that has committed an offence is checked. An offence is one of:
The check runs in the background, at the client's first request after the The check runs in the background, at the client's first request after the
offence that no ban refuses, so no request waits. The score applies to the offence that no ban refuses, so no request waits. The score applies to the
client's later requests. For an IPv6 client, AbuseIPDB is asked about the client's later requests. An IPv6 group is checked by the address of the request
address of the request that set off the check, and the score is used for the that has it checked, and the score is used for the whole group. A score at or
whole group. A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE` is a hit, and over `SWWAF_ABUSEIPDB_MIN_SCORE` is a hit, and `SWWAF_REPUTATION_ACTION` says
`SWWAF_REPUTATION_ACTION` says what is done. Each score is kept for what is done. Each score is kept for `SWWAF_REPUTATION_CACHE_TTL`.
`SWWAF_REPUTATION_CACHE_TTL`.
At most `SWWAF_ABUSEIPDB_DAILY_BUDGET` checks are made in a day, counted in UTC At most `SWWAF_ABUSEIPDB_DAILY_BUDGET` checks are made in a day, counted in UTC
and kept in `reputation.json`. A restart does not make the budget whole again. and kept in `reputation.json`. A restart does not make the budget whole again.
The check that spends the last of the budget is logged and alerted. No client is The check that spends the last of the budget is logged and alerted. No client is
checked until the day ends. A check fails when: checked until the day ends. A check fails when AbuseIPDB answers other than
`200`, gives no score, or does not answer within `SWWAF_REPUTATION_TIMEOUT`. A
- AbuseIPDB answers other than `200`; failure is logged and alerted, and no client is checked for a minute. The key is
- the answer gives no score; sent in the `Key` header and nowhere else, and is never logged.
- no answer comes within `SWWAF_REPUTATION_TIMEOUT`.
A failure is logged and alerted, and no client is checked for a minute. The key
is sent in the `Key` header and nowhere else, and is never logged.
## CrowdSec ## CrowdSec
@@ -1627,15 +1589,13 @@ A request is refused when a decision still in force names its client. The
client's netblock is banned until the decision ends, with the cause `crowdsec`. client's netblock is banned until the decision ends, with the cause `crowdsec`.
Only a client that sends a request is banned, so a long list does not fill Only a client that sends a request is banned, so a long list does not fill
`bans.json`. Such a ban is never made permanent, and does not lengthen the `bans.json`. Such a ban is never made permanent, and does not lengthen the
netblock's next ban for a broken limit. If you lift a ban while the decision is netblock's next ban for a broken limit. A ban you lift while the decision is in
in force, the client's next request makes another. Delete the decision in force is followed by another at the client's next request. Delete the decision
CrowdSec first, such as with `cscli decisions delete --ip 203.0.113.9`, and lift in CrowdSec first, such as with `cscli decisions delete --ip 203.0.113.9`, and
the ban once the list has been fetched again. A fetch fails when: lift the ban once the list has been fetched again. A fetch fails when the engine
answers other than `200`, a redirect included. It also fails when the fetch does
- the engine answers other than `200`, a redirect included; not finish within a minute, the answer is longer than 16 MiB, or the answer is
- the fetch does not finish within a minute; not a list of decisions.
- the answer is longer than 16 MiB;
- the answer is not a list of decisions.
## Rationale ## Rationale
@@ -1676,8 +1636,8 @@ candidates one by one.
or `SIGINT`. Then it stops, writing the state files. Run as or `SIGINT`. Then it stops, writing the state files. Run as
`smallwebwaf healthcheck`, it is the image's health check instead. `smallwebwaf healthcheck`, it is the image's health check instead.
- `internal/config`: reads the settings, the one place they are read. - `internal/config`: reads the settings, the one place they are read.
- `internal/proxy`: what happens to each request. It works out the client and - `internal/proxy`: what happens to each request. It works out the client, runs
runs the checks in `check`. It passes the request to the app with the standard the checks in `check`, and passes the request to the app with the standard
library's `httputil.ReverseProxy`, within the timeouts and size limits. It library's `httputil.ReverseProxy`, within the timeouts and size limits. It
answers its own endpoints in `answerAdmin`. Once the request has ended, it answers its own endpoints in `answerAdmin`. Once the request has ended, it
counts bytes, refusals and anomalies, and writes the log line. counts bytes, refusals and anomalies, and writes the log line.
@@ -1723,16 +1683,13 @@ Besides the standard library, it uses:
This repository adheres to the This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard. The scripts in `script/` are the entrypoints for working on it, and standard: the scripts in `script/` are the entrypoints for working on it, and
the `Makefile` targets are thin shims that call them. The scripts are POSIX sh. 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 what the other scripts need on the host: `make`,
`make bootstrap`. They need: `git`, `curl`, Go for `gofmt`, node and yarn, and prettier. `make bootstrap`.
- `make`, `git` and `curl`;
- Go, for `gofmt`;
- node, yarn and prettier.
- `script/setup`: readies a fresh clone: `script/bootstrap`, then - `script/setup`: readies a fresh clone: `script/bootstrap`, then
`script/install-precommit`. `make setup`. `script/install-precommit`. `make setup`.
- `script/projectname`: prints `smallwebwaf`, which the other scripts tag their - `script/projectname`: prints `smallwebwaf`, which the other scripts tag their