DNS blocklists asked in the background, verdicts kept (closes #104)
check / check (push) Waiting to run
check / check (push) Waiting to run
Zones in SWWAF_DNSBL_ZONES are asked about each client in the background, through SWWAF_DNSBL_RESOLVER or the host's resolver; no request waits. Verdicts last SWWAF_REPUTATION_CACHE_TTL, kept in reputation.json. SWWAF_REPUTATION_ACTION (limit:25) denies, limits or logs a listed client; each zone listing it raises reputation_hit. A failed query gives no verdict, raises source_failure, and pauses the zone a minute. A zone's key, its first label under dq.spamhaus.net, is masked everywhere but reputation.json. Zones compare without regard to case. Judgement call: answers in 127.255.255.0/24 or outside 127.0.0.0/8 are failures. Judgement call: the minute's pause after a failure; at most 1,000 queries at once. Judgement call: one zone given with two keys stops the start as listed twice. Rule suppressed: paralleltest on the DNSBL tests (Go's resolver shares state across synctest bubbles), funlen on the test of every logged setting. Model: opus-5-5
This commit is contained in:
@@ -26,33 +26,36 @@ Slack and ntfy, and remote log sending. So is the stage after that: the AS
|
||||
number and country of every client, looked up through GeoJS or in the IPinfo
|
||||
Lite database file, the byte limits, the biased thresholds, lower limits for the
|
||||
AS numbers and countries you list, and the anomaly thresholds, alerts for
|
||||
unusual traffic that refuse nothing. So is the first part of the stage after
|
||||
that: the blocklists you name by URL, which it fetches and keeps, and a file of
|
||||
AS numbers' percentages, fetched the same way. `smallwebwaf` passes each request
|
||||
to the app and the app's answer back, unchanged, within its timeouts and size
|
||||
limits, works out each client's address, looks up its AS number and country
|
||||
unless you switch that off, bans a client that sends too many requests or too
|
||||
many bytes, not counting those for the paths you choose, with lower limits for
|
||||
the clients of the AS numbers and countries you list, refuses a client that
|
||||
comes from a country you refuse or from a network you refuse, refuses, limits or
|
||||
only notes a client a blocklist you name lists, lets the networks you choose
|
||||
through, checks each request against the rule files and bans a client whose
|
||||
request is a clear sign of attack, keeps its bans, each client's counters and
|
||||
history, GeoJS's answers and the last good copy of each list it fetches in JSON
|
||||
files across restarts, takes in your edits of those files, such as a ban you
|
||||
make, keep or lift, and of the rule files while it runs, writes a JSON log line
|
||||
for every request, sends its log lines to a syslog server too if you name one,
|
||||
sends an alert to a webhook, to Slack and to ntfy, each if you name one, for
|
||||
each ban it makes or makes permanent, for traffic over an anomaly threshold you
|
||||
set, for a client a blocklist lists, for GeoJS failing or a list it cannot
|
||||
fetch, for a rule file or state file with an error and for a replacement of the
|
||||
lookup database it cannot read, serves Prometheus metrics to a scraper that
|
||||
holds the metrics token, lets an admin who holds the admin token list, add and
|
||||
lift bans and ask what it knows of a client, and in `observe` mode passes on the
|
||||
requests it would refuse, logging what it would have done with them. It comes as
|
||||
the image the app's own image is built on. The rest of the design comes after
|
||||
that, in the order of the build order in [`SPEC.md`](SPEC.md). The survey of
|
||||
existing tools that led to the design is in [`EVALUATION.md`](EVALUATION.md).
|
||||
unusual traffic that refuse nothing. So are the first two parts of the stage
|
||||
after that: the blocklists you name by URL, which it fetches and keeps, with a
|
||||
file of AS numbers' percentages fetched the same way, and the DNS blocklists
|
||||
(DNSBL zones), which it asks about each client in the background. `smallwebwaf`
|
||||
passes each request to the app and the app's answer back, unchanged, within its
|
||||
timeouts and size limits, works out each client's address, looks up its AS
|
||||
number and country unless you switch that off, bans a client that sends too many
|
||||
requests or too many bytes, not counting those for the paths you choose, with
|
||||
lower limits for the clients of the AS numbers and countries you list, refuses a
|
||||
client that comes from a country you refuse or from a network you refuse,
|
||||
refuses, limits or only notes a client a blocklist or a DNSBL zone you name
|
||||
lists, lets the networks you choose through, checks each request against the
|
||||
rule files and bans a client whose request is a clear sign of attack, keeps its
|
||||
bans, each client's counters and history, GeoJS's answers, the last good copy of
|
||||
each list it fetches and the DNSBL zones' verdicts in JSON files across
|
||||
restarts, takes in your edits of those files, such as a ban you make, keep or
|
||||
lift, and of the rule files while it runs, writes a JSON log line for every
|
||||
request, sends its log lines to a syslog server too if you name one, sends an
|
||||
alert to a webhook, to Slack and to ntfy, each if you name one, for each ban it
|
||||
makes or makes permanent, for traffic over an anomaly threshold you set, for a
|
||||
client a blocklist or a DNSBL zone lists, for GeoJS failing, a list it cannot
|
||||
fetch or a DNSBL zone that fails or refuses a query, for a rule file or state
|
||||
file with an error and for a replacement of the lookup database it cannot read,
|
||||
serves Prometheus metrics to a scraper that holds the metrics token, lets an
|
||||
admin who holds the admin token list, add and lift bans and ask what it knows of
|
||||
a client, and in `observe` mode passes on the requests it would refuse, logging
|
||||
what it would have done with them. It comes as the image the app's own image is
|
||||
built on. The rest of the design comes after that, in the order of the build
|
||||
order in [`SPEC.md`](SPEC.md). The survey of existing tools that led to the
|
||||
design is in [`EVALUATION.md`](EVALUATION.md).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -206,35 +209,48 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
|
||||
limit, the lowest of its percentages applying, as for a biased threshold, and
|
||||
with `log` nothing more is done. Whatever the action, the request's log line
|
||||
names the lists, and each raises an alert.
|
||||
- Checks the client's own address against the DNSBL zones `SWWAF_DNSBL_ZONES`
|
||||
names, after the blocklists and before the rate limits (see "DNS blocklists"
|
||||
below), by the verdicts it keeps. A zone is asked about a client in the
|
||||
background, and no request waits for its answer: the request that has it
|
||||
asked, and any other from the client before the answer comes, goes on as from
|
||||
a client the zone does not list. With `SWWAF_REPUTATION_ACTION` at `limit:25`,
|
||||
its default, a client a zone's verdict lists gets a quarter of every rate
|
||||
limit and byte limit, the lowest of its percentages applying, as for a biased
|
||||
threshold. With `deny`, its requests are refused with `SWWAF_BAN_RESPONSE`
|
||||
before their bodies are read; they are not counted for the rate limits, and
|
||||
make no ban. With `log`, nothing more is done. Whatever the action, the
|
||||
request's log line names the zones, and each raises an alert. A client a
|
||||
blocklist refuses is not checked.
|
||||
- Checks the client's own address against the static lists, the three netblock
|
||||
settings below, before anything else, its lookup included. A client in
|
||||
`SWWAF_ALLOW_NETS` skips bans, the country lists, the blocklists, the rate
|
||||
limits, the byte limits and the rule files, and is not looked up; the timeouts
|
||||
and size limits still apply. A client in `SWWAF_DENY_NETS` is refused with
|
||||
`SWWAF_BAN_RESPONSE` before its body is read, and the request is not counted
|
||||
for the rate limits; an address in `SWWAF_ALLOW_NETS` too is let through. A
|
||||
client in `SWWAF_RATE_LIMIT_EXEMPT_NETS` is neither counted nor refused by the
|
||||
rate limits, and has no bytes counted by the byte limits; the country lists,
|
||||
the rule files and bans still apply to it.
|
||||
`SWWAF_ALLOW_NETS` skips bans, the country lists, the blocklists, the DNSBL
|
||||
zones, the rate limits, the byte limits and the rule files, and is not looked
|
||||
up; the timeouts and size limits still apply. A client in `SWWAF_DENY_NETS` is
|
||||
refused with `SWWAF_BAN_RESPONSE` before its body is read, and the request is
|
||||
not counted for the rate limits; an address in `SWWAF_ALLOW_NETS` too is let
|
||||
through. A client in `SWWAF_RATE_LIMIT_EXEMPT_NETS` is neither counted nor
|
||||
refused by the rate limits, and has no bytes counted by the byte limits; the
|
||||
country lists, the rule files and bans still apply to it.
|
||||
- In `observe` mode, with `SWWAF_MODE=observe`, refuses none of the requests
|
||||
that `SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, a rate limit or
|
||||
a rule would refuse: it passes them to the app, and their log lines name what
|
||||
`enforce` mode would have done (see `would_action` in "Request log" below).
|
||||
The checks run, and requests and bytes are counted, as in `enforce` mode, with
|
||||
three differences: neither a broken rate limit or byte limit nor a `ban` rule
|
||||
makes a ban; a broken limit does not set the client's counters back to zero,
|
||||
so each request over a rate limit is logged as one that would be refused, and
|
||||
each whose bytes keep the client over a byte limit as breaking it; and a
|
||||
request under a ban does not make it permanent. As in `enforce` mode, the
|
||||
bytes counted are only those of the requests `enforce` mode would have passed
|
||||
to the app. A ban it would have made, or made permanent, raises the alert
|
||||
`enforce` mode would have raised, marked as what would have happened (see
|
||||
"Alerts" below). The bans in `bans.json` are kept, and refuse requests again
|
||||
when `smallwebwaf` next runs in `enforce` mode, as long as they last. The
|
||||
timeouts and size limits still apply, since they protect `smallwebwaf` and the
|
||||
app themselves, and a request for one of `smallwebwaf`'s own endpoints without
|
||||
its token is still answered `401`. It is for trying a configuration before
|
||||
enforcing it.
|
||||
that `SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, a DNSBL zone's
|
||||
verdict, a rate limit or a rule would refuse: it passes them to the app, and
|
||||
their log lines name what `enforce` mode would have done (see `would_action`
|
||||
in "Request log" below). The checks run, and requests and bytes are counted,
|
||||
as in `enforce` mode, with three differences: neither a broken rate limit or
|
||||
byte limit nor a `ban` rule makes a ban; a broken limit does not set the
|
||||
client's counters back to zero, so each request over a rate limit is logged as
|
||||
one that would be refused, and each whose bytes keep the client over a byte
|
||||
limit as breaking it; and a request under a ban does not make it permanent. As
|
||||
in `enforce` mode, the bytes counted are only those of the requests `enforce`
|
||||
mode would have passed to the app. A ban it would have made, or made
|
||||
permanent, raises the alert `enforce` mode would have raised, marked as what
|
||||
would have happened (see "Alerts" below). The bans in `bans.json` are kept,
|
||||
and refuse requests again when `smallwebwaf` next runs in `enforce` mode, as
|
||||
long as they last. The timeouts and size limits still apply, since they
|
||||
protect `smallwebwaf` and the app themselves, and a request for one of
|
||||
`smallwebwaf`'s own endpoints without its token is still answered `401`. It is
|
||||
for trying a configuration before enforcing it.
|
||||
- Answers `GET /_smallwebwaf/healthz` itself with `200` and `ok`, before any
|
||||
check and without asking the app, for the image's health check.
|
||||
- Answers `GET /_smallwebwaf/metrics` with its metrics (see "Metrics" below) for
|
||||
@@ -254,14 +270,14 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
|
||||
`SWWAF_LOG_REMOTE_URL` names one (see "Sending the log to a syslog server"
|
||||
below).
|
||||
- Sends an alert for each ban it makes or makes permanent, for a count over an
|
||||
anomaly threshold, for a client a blocklist lists, for GeoJS failing or a list
|
||||
it cannot fetch, for a rule file or state file with an error, and for a
|
||||
replacement of the lookup database it cannot read, holding back repeats and,
|
||||
past an hourly limit, rolling the rest into one summary, to each destination
|
||||
you name: as a JSON object to the webhook `SWWAF_ALERT_WEBHOOK_URL` names, as
|
||||
a message to the Slack incoming webhook `SWWAF_ALERT_SLACK_WEBHOOK_URL` names,
|
||||
and as a message to the ntfy topic `SWWAF_ALERT_NTFY_URL` names (see "Alerts"
|
||||
below).
|
||||
anomaly threshold, for a client a blocklist or a DNSBL zone lists, for GeoJS
|
||||
failing, a list it cannot fetch or a DNSBL zone that fails or refuses a query,
|
||||
for a rule file or state file with an error, and for a replacement of the
|
||||
lookup database it cannot read, holding back repeats and, past an hourly
|
||||
limit, rolling the rest into one summary, to each destination you name: as a
|
||||
JSON object to the webhook `SWWAF_ALERT_WEBHOOK_URL` names, as a message to
|
||||
the Slack incoming webhook `SWWAF_ALERT_SLACK_WEBHOOK_URL` names, and as a
|
||||
message to the ntfy topic `SWWAF_ALERT_NTFY_URL` names (see "Alerts" below).
|
||||
- Counts requests and their bytes over a minute and an hour, per client, per
|
||||
netblock around a client, per AS number, for the whole service and per named
|
||||
netblock, and sends an `anomaly` alert for a count over the anomaly threshold
|
||||
@@ -324,8 +340,8 @@ effective settings are logged at start.
|
||||
- `SWWAF_REQUEST_MAX_BYTES` (default `100M`): the largest request body.
|
||||
- `SWWAF_RESPONSE_MAX_BYTES` (default `5G`): the largest response body.
|
||||
- `SWWAF_ALLOW_NETS` (default empty): netblocks whose clients skip bans, the
|
||||
country lists, the blocklists, the rate limits, the byte limits and the rule
|
||||
files, such as your monitoring or your own networks.
|
||||
country lists, the blocklists, the DNSBL zones, the rate limits, the byte
|
||||
limits and the rule files, such as your monitoring or your own networks.
|
||||
- `SWWAF_RATE_LIMIT_EXEMPT_NETS` (default empty): netblocks whose clients the
|
||||
rate limits and the byte limits do not apply to, such as a machine that talks
|
||||
to the app all day.
|
||||
@@ -427,10 +443,34 @@ effective settings are logged at start.
|
||||
`limit:<percent>`, such as `limit:25`, gives it that percentage of every rate
|
||||
limit and byte limit, and `log` does nothing more than note the lists in the
|
||||
log line and raise the alert.
|
||||
- `SWWAF_DNSBL_ZONES` (default empty): the DNSBL zones each client is asked
|
||||
about, such as `dnsbl.dronebl.org` (see "DNS blocklists" below). A zone is a
|
||||
DNS name of at most 189 characters: labels of letters, digits and hyphens, of
|
||||
up to 63 characters each, neither starting nor ending with a hyphen, joined by
|
||||
dots. A Spamhaus zone is the name of its keyed query service, with the key in
|
||||
it, such as `<key>.xbl.dq.spamhaus.net`, and is shown with its key masked (see
|
||||
"DNS blocklists" below). A zone that is not such a name stops the start, as
|
||||
does one listed twice, even with its letters in another case or with another
|
||||
key. Do not name a zone meant for mail (see "DNS blocklists" below).
|
||||
- `SWWAF_DNSBL_RESOLVER` (default unset): the resolver the zones are asked
|
||||
through, an IP address with an optional port, 53 when none is given, such as
|
||||
`192.0.2.53` or `[2001:db8::53]:5353`. Unset, it is the host's, as
|
||||
`/etc/resolv.conf` names it. Several zones refuse queries that come through a
|
||||
public resolver.
|
||||
- `SWWAF_REPUTATION_ACTION` (default `limit:25`): what is done with a client a
|
||||
zone's verdict lists, as `SWWAF_BLOCKLIST_ACTION` is for a blocklist: `deny`,
|
||||
`limit:<percent>` or `log`. A zone's verdict is less certain than a list such
|
||||
as DROP, so by default a client it lists gets a quarter of every rate limit
|
||||
and byte limit.
|
||||
- `SWWAF_REPUTATION_CACHE_TTL` (default `24h`): how long a zone's verdict on a
|
||||
client is used after the zone gave it.
|
||||
- `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone may take
|
||||
before it fails.
|
||||
- `SWWAF_BAN_RESPONSE` (default `403`): how a refused client is answered, one
|
||||
that is banned, breaks a rate limit, matches a `ban` rule, is in
|
||||
`SWWAF_DENY_NETS`, comes from a refused country or is in a blocklist while
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny`: `403`, `429`, or `close` to close the
|
||||
`SWWAF_DENY_NETS`, comes from a refused country, is in a blocklist while
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny` or is listed by a DNSBL zone while
|
||||
`SWWAF_REPUTATION_ACTION` is `deny`: `403`, `429`, or `close` to close the
|
||||
connection without an answer. Behind traefik, `close` does not leave the
|
||||
client unanswered: traefik answers `502`, as it does whenever its backend
|
||||
drops a connection. A `block` rule always answers `403`.
|
||||
@@ -576,14 +616,16 @@ listed twice in one of them stops the start. `off` switches a timeout, a size
|
||||
limit, a rate limit, a byte limit, an anomaly threshold, `SWWAF_ALERT_COOLDOWN`
|
||||
or `SWWAF_ALERT_MAX_PER_HOUR` off; `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES`,
|
||||
`SWWAF_LOOKUP_TIMEOUT`, `SWWAF_UNKNOWN_LIMIT_PERCENT`,
|
||||
`SWWAF_BLOCKLIST_REFRESH`, the ban settings, the state settings,
|
||||
`SWWAF_BLOCKLIST_REFRESH`, `SWWAF_REPUTATION_CACHE_TTL`,
|
||||
`SWWAF_REPUTATION_TIMEOUT`, the ban settings, the state settings,
|
||||
`SWWAF_METRICS_TOP_N`, `SWWAF_LOG_REMOTE_BUFFER`, `SWWAF_ANOMALY_NET_V4_PREFIX`
|
||||
and `SWWAF_ANOMALY_NET_V6_PREFIX` cannot be off.
|
||||
|
||||
Several limits are fixed rather than settings. At most 20,000 clients are kept,
|
||||
with their counters and history, and an IPv6 client is counted by its /64. At
|
||||
most 100,000 answers from GeoJS are kept, for 7 days each, and at most 20,000
|
||||
anomaly counters.
|
||||
most 100,000 answers from GeoJS are kept, for 7 days each, at most 20,000
|
||||
anomaly counters, and at most 100,000 verdicts of the DNSBL zones, with at most
|
||||
1,000 queries to them under way at once.
|
||||
|
||||
### Settings given as files
|
||||
|
||||
@@ -666,8 +708,9 @@ which every line has.
|
||||
app's, as passed on, or those of `smallwebwaf`'s own answer.
|
||||
- `request_bytes` and `response_bytes` count body bytes.
|
||||
- `action` is `forward` for a request passed to the app, `denied` for one
|
||||
refused because its client is in `SWWAF_DENY_NETS`, or in a blocklist while
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny`, `banned` for one refused because a ban
|
||||
refused because its client is in `SWWAF_DENY_NETS`, in a blocklist while
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny`, or listed by a DNSBL zone while
|
||||
`SWWAF_REPUTATION_ACTION` is `deny`, `banned` for one refused because a ban
|
||||
covers its client or because it matched a `ban` rule, which bans its client,
|
||||
`country_denied` for one refused for its client's country, `rate_limited` for
|
||||
one that broke a rate limit and banned its client, `rule_blocked` for one a
|
||||
@@ -676,16 +719,17 @@ which every line has.
|
||||
could not be reached or its answer broke off, and `admin` for one
|
||||
`smallwebwaf` answered at its own endpoint.
|
||||
- `would_action` is there in `observe` mode for a request that
|
||||
`SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, a rate limit or a
|
||||
rule would have refused in `enforce` mode, and names the action that refusal
|
||||
would have had: `denied`, `banned`, `country_denied`, `rate_limited` or
|
||||
`rule_blocked`. `action` then names what was done: `forward` for a request
|
||||
passed to the app, and another action, such as `too_large`, for one a size or
|
||||
time limit refused.
|
||||
`SWWAF_DENY_NETS`, a ban, the country lists, a blocklist, a DNSBL zone's
|
||||
verdict, a rate limit or a rule would have refused in `enforce` mode, and
|
||||
names the action that refusal would have had: `denied`, `banned`,
|
||||
`country_denied`, `rate_limited` or `rule_blocked`. `action` then names what
|
||||
was done: `forward` for a request passed to the app, and another action, such
|
||||
as `too_large`, for one a size or time limit refused.
|
||||
- `limit_percent` is there for a request the rate limits count whose client a
|
||||
biased threshold, or `SWWAF_BLOCKLIST_ACTION` for a blocklist that lists it,
|
||||
gives less than the whole of the rate limits, and gives the percentage it
|
||||
gets, with `limit_percent_setting` naming the setting that gave it, such as
|
||||
biased threshold, `SWWAF_BLOCKLIST_ACTION` for a blocklist that lists it, or
|
||||
`SWWAF_REPUTATION_ACTION` for a DNSBL zone whose verdict lists it, gives less
|
||||
than the whole of the rate limits, and gives the percentage it gets, with
|
||||
`limit_percent_setting` naming the setting that gave it, such as
|
||||
`SWWAF_ASN_LIMIT_PERCENT`, or `SWWAF_ASN_LIMIT_PERCENT_URL` for the file it
|
||||
names. `bytes_percent` and `bytes_percent_setting` are the same for the byte
|
||||
limits. Each is left out when the client gets the whole of those limits.
|
||||
@@ -697,11 +741,11 @@ which every line has.
|
||||
health check, one from a client in `SWWAF_ALLOW_NETS` or
|
||||
`SWWAF_RATE_LIMIT_EXEMPT_NETS`, one for a path that
|
||||
`SWWAF_RATE_LIMIT_EXEMPT_PATHS` exempts, and one that `SWWAF_DENY_NETS`, a
|
||||
ban, the country lists or a blocklist refuse, or would refuse in `observe`
|
||||
mode. Its `minute_bytes`, `hour_bytes` and `day_bytes` give the client's bytes
|
||||
in each window as the byte limits count them, in the same way: for a request
|
||||
whose bytes they count, with its own, once it has ended; for any other, those
|
||||
counted before it.
|
||||
ban, the country lists, a blocklist or a DNSBL zone's verdict refuse, or would
|
||||
refuse in `observe` mode. Its `minute_bytes`, `hour_bytes` and `day_bytes`
|
||||
give the client's bytes in each window as the byte limits count them, in the
|
||||
same way: for a request whose bytes they count, with its own, once it has
|
||||
ended; for any other, those counted before it.
|
||||
- `rule_ids` is there for a request that matched rules of the rule files, and
|
||||
lists their ids in the order they matched, up to the one that refused it.
|
||||
- `limit_hit` is there for a request that broke a rate limit, or whose bytes
|
||||
@@ -711,12 +755,16 @@ which every line has.
|
||||
several. `offence` is then `limit`. A request whose bytes broke a byte limit
|
||||
is not refused: its `action` is what it would have been otherwise, such as
|
||||
`forward`.
|
||||
- `reputation` is there for a request whose client a blocklist lists, and gives
|
||||
the URLs of the blocklists that list it, in the order `SWWAF_BLOCKLIST_URLS`
|
||||
names them, whatever `SWWAF_BLOCKLIST_ACTION` says. It is left out for a
|
||||
client the blocklists are not checked for: one in `SWWAF_ALLOW_NETS`, and one
|
||||
`SWWAF_DENY_NETS`, a ban or the country lists refuse first, or would in
|
||||
`observe` mode.
|
||||
- `reputation` is there for a request whose client a blocklist or a DNSBL zone's
|
||||
verdict lists, and gives the URLs of the blocklists that list it, in the order
|
||||
`SWWAF_BLOCKLIST_URLS` names them, then the zones whose verdict lists it, in
|
||||
the order `SWWAF_DNSBL_ZONES` names them, whatever `SWWAF_BLOCKLIST_ACTION`
|
||||
and `SWWAF_REPUTATION_ACTION` say. A zone whose verdict on the client has not
|
||||
come yet, or was given `SWWAF_REPUTATION_CACHE_TTL` ago or more, is not named.
|
||||
It is left out for a client the blocklists are not checked for: one in
|
||||
`SWWAF_ALLOW_NETS`, and one `SWWAF_DENY_NETS`, a ban or the country lists
|
||||
refuse first, or would in `observe` mode. The zones are not checked either for
|
||||
a client a blocklist refuses, or would in `observe` mode.
|
||||
- `ban_expires` is there for a request that made a ban or was refused under one,
|
||||
or in `observe` mode would have been refused under one, and gives when the ban
|
||||
ends, in the same form as `time`, or `permanent`.
|
||||
@@ -786,11 +834,13 @@ it, as below. An alert is for one of these events, and is sent when
|
||||
- `anomaly`: a count of requests or bytes over an anomaly threshold, raised by
|
||||
each request that ends with the count over it, in `observe` mode as in
|
||||
`enforce` mode. It refuses and bans nothing.
|
||||
- `reputation_hit`: a request whose client a blocklist lists, one alert for each
|
||||
blocklist that lists it, whatever `SWWAF_BLOCKLIST_ACTION` says, in `observe`
|
||||
mode as in `enforce` mode.
|
||||
- `source_failure`: GeoJS failing or refusing `smallwebwaf`, or a fetch of a
|
||||
list failing (see "Blocklists" below).
|
||||
- `reputation_hit`: a request whose client a blocklist or a DNSBL zone's verdict
|
||||
lists, one alert for each blocklist and each zone that lists it, whatever
|
||||
`SWWAF_BLOCKLIST_ACTION` and `SWWAF_REPUTATION_ACTION` say, in `observe` mode
|
||||
as in `enforce` mode.
|
||||
- `source_failure`: GeoJS failing or refusing `smallwebwaf`, a fetch of a list
|
||||
failing (see "Blocklists" below), or a query to a DNSBL zone failing or
|
||||
refused (see "DNS blocklists" below).
|
||||
- `file_error`: a rule file edited while it runs that has an error, an edit of a
|
||||
state file set aside as `<name>.bad`, a state file it could not write while
|
||||
running, or a replacement of the lookup database it could not read, which it
|
||||
@@ -863,7 +913,7 @@ is sent on one line:
|
||||
- `reason` is a short sentence; for a ban, the ban's `reason` in `bans.json`;
|
||||
for an `anomaly`, what was counted over which threshold, such as
|
||||
`requests per minute of the netblock 203.0.113.0/24 over the threshold of 1000`;
|
||||
for a `reputation_hit`, `listed by a blocklist`.
|
||||
for a `reputation_hit`, `listed by a blocklist` or `listed by a DNSBL zone`.
|
||||
- `detail` is what is particular to the event: for a ban, its `cause`, when it
|
||||
ends as `ban_expires`, in the form the request log gives it, and its `notes`,
|
||||
as `bans.json` gives them; for an `anomaly`, the `scope`, `client`, `net`,
|
||||
@@ -871,11 +921,11 @@ is sent on one line:
|
||||
`asn` and the `name` of the named netblock for `watch`, the `window`, `minute`
|
||||
or `hour`, the `kind`, `requests` or `bytes`, the `count`, which is weighted
|
||||
as the rate limits weigh theirs, and the `threshold`; for a `reputation_hit`,
|
||||
the `source`, the URL of the blocklist; for `source_failure`, the `source`,
|
||||
`geojs` or the URL of the list, the `error`, and for GeoJS, when it is asked
|
||||
again, `asking_again_in`; for `file_error`, the `file`, which for an edit set
|
||||
aside is the file it was renamed to, and the `error`, which for a file that
|
||||
does not parse names where in it the error is.
|
||||
the `source`, the URL of the blocklist or the zone; for `source_failure`, the
|
||||
`source`, `geojs`, the URL of the list or the zone, the `error`, and for
|
||||
GeoJS, when it is asked again, `asking_again_in`; for `file_error`, the
|
||||
`file`, which for an edit set aside is the file it was renamed to, and the
|
||||
`error`, which for a file that does not parse names where in it the error is.
|
||||
- `suppressed_repeats` is how many repeats the cooldown held back before this
|
||||
alert, and for a `summary`, those no other alert gives (see below).
|
||||
|
||||
@@ -921,11 +971,11 @@ and Slack this JSON object, shown indented; it is sent on one line:
|
||||
```
|
||||
|
||||
An alert for the same event as the last one sent, on the same netblock, and for
|
||||
a `reputation_hit` about the same blocklist, or for a `file_error` about the
|
||||
same file, or for a `source_failure` about the same source, or for an `anomaly`
|
||||
in the same scope, with the same netblock, AS number or name, whatever its
|
||||
window and kind, less than `SWWAF_ALERT_COOLDOWN` after it, is a repeat: it is
|
||||
held back and counted, and the next alert sent for them gives that count as
|
||||
a `reputation_hit` about the same blocklist or zone, or for a `file_error` about
|
||||
the same file, or for a `source_failure` about the same source, or for an
|
||||
`anomaly` in the same scope, with the same netblock, AS number or name, whatever
|
||||
its window and kind, less than `SWWAF_ALERT_COOLDOWN` after it, is a repeat: it
|
||||
is held back and counted, and the next alert sent for them gives that count as
|
||||
`suppressed_repeats`. As each hour of the clock, in UTC, ends, the cooldowns
|
||||
that have run out are dropped, and the repeats they held back, which no alert
|
||||
sent since has given, go in that hour's summary.
|
||||
@@ -990,8 +1040,11 @@ with times in UTC.
|
||||
indented to be read, under `lists`: its `url`, when it was last `tried`, the
|
||||
fetch failed or not, and its last good copy: when that was `fetched`, and its
|
||||
`lines`, as fetched, comment lines included, each on a line of its own, both
|
||||
left out while no fetch of it has succeeded. As the file is read, the lists
|
||||
the settings no longer name are dropped.
|
||||
left out while no fetch of it has succeeded; and under `verdicts`, each
|
||||
verdict of a DNSBL zone still in use (see "DNS blocklists" below): its `zone`,
|
||||
the `client`'s address, whether the zone `listed` the client, and when the
|
||||
zone gave it, `fetched`. As the file is read, the lists the settings no longer
|
||||
name, and the verdicts of the zones they no longer name, are dropped.
|
||||
- `alerts.json`: the state of the alerts (see "Alerts" above), indented to be
|
||||
read: under `cooldowns`, for each event and netblock, with the `source` too
|
||||
for a `reputation_hit`, or event and `file` or `source`, or for an `anomaly`,
|
||||
@@ -1029,24 +1082,26 @@ keeps refusing every client in its netblock until it ends, even after
|
||||
until a fetch of it succeeds. A netblock whose address has bits past its length,
|
||||
such as `203.0.113.9/24`, is read as the netblock it is in, `203.0.113.0/24`.
|
||||
Buckets and answers whose time has passed are dropped, and so is an anomaly
|
||||
counter left with no bucket. A missing file is empty state, as on a first start.
|
||||
A file that does not parse, or has another `version`, stops the start with a
|
||||
message naming the file, and the line and column where Go's JSON decoder gives
|
||||
them; so does a state directory `smallwebwaf` cannot write. So does an entry
|
||||
without a field it needs, named with the entry's place in the file: a ban's
|
||||
`netblock`, `start` or `expires`, which is `null` for a permanent ban; a
|
||||
client's `client`, or the `start` of a window in which it has requests or bytes;
|
||||
an answer's `client`, `country`, which is `""` for a client GeoJS cannot place,
|
||||
or `answered`; a cooldown's `event` or `sent`; an alert waiting's `event` or
|
||||
`time`; an anomaly counter's `netblock`, unless it counts an AS number or the
|
||||
whole service, its `asn`, for an AS number, its `name`, for a named netblock, or
|
||||
the `start` of a window in which it has requests or bytes; a list's `url`,
|
||||
`fetched` or `lines`, which is `[]` for an empty list. So does a ban whose
|
||||
`cause` is not `limit`, `attack` or `admin`, alerts waiting for a destination
|
||||
that is not `webhook`, `slack` or `ntfy`, an anomaly counter whose `scope` is
|
||||
not `client`, `net`, `asn`, `total` or `watch`, and a copy of a list with a line
|
||||
that would make its fetch fail. An answer's `asn` or `as_name` left out reads as
|
||||
empty.
|
||||
counter left with no bucket; a verdict past its time is neither used nor written
|
||||
again. A missing file is empty state, as on a first start. A file that does not
|
||||
parse, or has another `version`, stops the start with a message naming the file,
|
||||
and the line and column where Go's JSON decoder gives them; so does a state
|
||||
directory `smallwebwaf` cannot write. So does an entry without a field it needs,
|
||||
named with the entry's place in the file: a ban's `netblock`, `start` or
|
||||
`expires`, which is `null` for a permanent ban; a client's `client`, or the
|
||||
`start` of a window in which it has requests or bytes; an answer's `client`,
|
||||
`country`, which is `""` for a client GeoJS cannot place, or `answered`; a
|
||||
cooldown's `event` or `sent`; an alert waiting's `event` or `time`; an anomaly
|
||||
counter's `netblock`, unless it counts an AS number or the whole service, its
|
||||
`asn`, for an AS number, its `name`, for a named netblock, or the `start` of a
|
||||
window in which it has requests or bytes; a list's `url`, `fetched` or `lines`,
|
||||
which is `[]` for an empty list; a verdict's `zone`, `client`, `listed`, which
|
||||
is `false` for a client the zone does not list, or `fetched`. So does a ban
|
||||
whose `cause` is not `limit`, `attack` or `admin`, alerts waiting for a
|
||||
destination that is not `webhook`, `slack` or `ntfy`, an anomaly counter whose
|
||||
`scope` is not `client`, `net`, `asn`, `total` or `watch`, and a copy of a list
|
||||
with a line that would make its fetch fail. An answer's `asn` or `as_name` left
|
||||
out reads as empty.
|
||||
|
||||
While it runs, `smallwebwaf` watches `SWWAF_STATE_DIR` and takes in your edit of
|
||||
a state file as soon as you save it: what the file then holds replaces what
|
||||
@@ -1238,6 +1293,11 @@ scraped, and keeps this one as `exported_instance` unless the scrape sets
|
||||
`smallwebwaf_reputation_failures_total`: the fetches of the list that failed;
|
||||
and `smallwebwaf_reputation_last_fetch_timestamp_seconds`: when the copy of it
|
||||
in use was fetched, `0` while there is none.
|
||||
- By `source`, each zone `SWWAF_DNSBL_ZONES` names:
|
||||
`smallwebwaf_reputation_hits_total`: the requests whose client the zone's
|
||||
verdict lists, a series that comes with the first;
|
||||
`smallwebwaf_reputation_queries_total`: the queries made to the zone; and
|
||||
`smallwebwaf_reputation_failures_total`: those that failed.
|
||||
- `smallwebwaf_tracked_clients`: the clients in the table of clients.
|
||||
- `smallwebwaf_state_file_writes_total`,
|
||||
`smallwebwaf_state_file_write_failures_total`,
|
||||
@@ -1429,9 +1489,9 @@ goes through the candidates one by one.
|
||||
readable JSON files, written regularly and at every stop, so a restart loses
|
||||
nothing. Edit a file, or add a rule file, and the running `smallwebwaf` picks
|
||||
up the change. Nothing is read from disk while serving a request. The files
|
||||
for the bans, the clients, the GeoJS answers, the copies of the lists and the
|
||||
alerts are built, with an edit taken in while running (see "State files"
|
||||
above); the rest comes with its features.
|
||||
for the bans, the clients, the GeoJS answers, the copies of the lists, the
|
||||
verdicts of the DNSBL zones and the alerts are built, with an edit taken in
|
||||
while running (see "State files" above); the rest comes with its features.
|
||||
- Health checks, the metrics, and listing, adding and lifting bans or asking why
|
||||
a given address was refused, all on the one port every request uses: under
|
||||
`/_smallwebwaf/` on the app's own address, through traefik like any other
|
||||
@@ -1659,6 +1719,58 @@ hour apart whatever `SWWAF_BLOCKLIST_REFRESH` is: each fetches a list again that
|
||||
long after its own last try, so those first started within the same hour, with
|
||||
the same refresh, keep fetching within the same hour.
|
||||
|
||||
## DNS blocklists
|
||||
|
||||
`SWWAF_DNSBL_ZONES` names DNS blocklists, DNSBL zones, which list an address by
|
||||
answering a query for a name made from it. None is named by default, for the
|
||||
reason none of the blocklists is. `smallwebwaf` asks each zone about a client's
|
||||
own address in the background, the first time it sees the client: the request
|
||||
goes on at once, as from a client the zone does not list, and so does every
|
||||
request from it until the zone has answered. The name asked about is the one RFC
|
||||
5782 gives: the four numbers of an IPv4 address in reverse order, so that
|
||||
`192.0.2.99` is asked about in `dnsbl.dronebl.org` as
|
||||
`99.2.0.192.dnsbl.dronebl.org`, or the 32 hex digits of an IPv6 address in
|
||||
reverse order, each followed by a dot. An IPv6 client is asked about by its own
|
||||
address, not by its /64.
|
||||
|
||||
A zone that answers that the name does not exist, or has no address, does not
|
||||
list the client, and one that answers with an address in `127.0.0.0/8` lists it.
|
||||
Any other answer gives no verdict, and is a failure: an address in
|
||||
`127.255.255.0/24`, with which Spamhaus refuses a query, such as one sent
|
||||
through a public resolver or one past its limit; an address outside
|
||||
`127.0.0.0/8`, such as a resolver gives that answers even for names that do not
|
||||
exist; an error the resolver answers with, such as a refusal; and no answer
|
||||
within `SWWAF_REPUTATION_TIMEOUT`. A failure is counted, logged and raised as a
|
||||
`source_failure` alert, held back as a repeat within `SWWAF_ALERT_COOLDOWN`, and
|
||||
the zone is not asked again for a minute, so that a zone that refuses queries is
|
||||
not asked on every request.
|
||||
|
||||
Each verdict, the zone listing the client or not, is kept, in memory and in
|
||||
`reputation.json` (see "State files" above), so that a restart keeps it, and is
|
||||
used for `SWWAF_REPUTATION_CACHE_TTL` after the zone gave it, 24 hours by
|
||||
default. The client's first request after that has the zone asked again, and
|
||||
until it answers, no verdict of the zone applies to the client. At most 100,000
|
||||
verdicts are kept, the one fetched longest ago dropped first, and at most 1,000
|
||||
queries are under way at once; past that, a zone is asked about a client at the
|
||||
client's next request.
|
||||
|
||||
A client in `SWWAF_ALLOW_NETS` is not checked, nor is one a blocklist refuses.
|
||||
`SWWAF_REPUTATION_ACTION` says what is done with one a zone's verdict lists, as
|
||||
"What it does so far" above describes.
|
||||
|
||||
Do not name a zone meant for mail, such as one of residential and dynamic
|
||||
address ranges, which ordinary visitors come from, or one that includes such a
|
||||
list, as Spamhaus's `zen` does: it would refuse them, or lower their limits.
|
||||
Spamhaus's zones answer through its keyed query service, named with the key in
|
||||
it, such as `<key>.xbl.dq.spamhaus.net`. The key of a zone under
|
||||
`dq.spamhaus.net` is its first label, and `smallwebwaf` shows `********` in its
|
||||
place wherever it names the zone, as `********.xbl.dq.spamhaus.net`: in the
|
||||
settings logged at start, an error that stops the start, its own messages, the
|
||||
request log, the alerts and the metrics. Only `reputation.json` keeps the zone
|
||||
with its key. A key in the name of any other zone is shown as given. Several
|
||||
zones refuse queries that come through a public resolver; `SWWAF_DNSBL_RESOLVER`
|
||||
names another resolver to ask through.
|
||||
|
||||
## How the code is laid out
|
||||
|
||||
- `cmd/smallwebwaf`: the binary, which only calls `internal/smallwebwaf`.
|
||||
@@ -1672,15 +1784,15 @@ the same refresh, keep fetching within the same hour.
|
||||
standard library's `httputil.ReverseProxy` within the timeouts and size
|
||||
limits, and writes the request's log line. Its `check` method is where a
|
||||
request is refused before anything reaches the app: for `SWWAF_DENY_NETS`, for
|
||||
a ban, for the country lists, for a blocklist, for a rate limit, which bans
|
||||
the client, for a `block` or `ban` rule, the latter banning the client, and
|
||||
for an announced body over the size limit; in `observe` mode, only for the
|
||||
size limit, with what it would have refused for noted in the log line. A
|
||||
request under `/_smallwebwaf/` that `check` lets through is answered by
|
||||
`answerAdmin` instead of reaching the app. Once the answer to a request passed
|
||||
to the app has ended, `countBytes` counts its bytes for the byte limits, and
|
||||
once any request but the health check has ended, `countAnomalies` counts it
|
||||
for the anomaly thresholds.
|
||||
a ban, for the country lists, for a blocklist, for a DNSBL zone's verdict, for
|
||||
a rate limit, which bans the client, for a `block` or `ban` rule, the latter
|
||||
banning the client, and for an announced body over the size limit; in
|
||||
`observe` mode, only for the size limit, with what it would have refused for
|
||||
noted in the log line. A request under `/_smallwebwaf/` that `check` lets
|
||||
through is answered by `answerAdmin` instead of reaching the app. Once the
|
||||
answer to a request passed to the app has ended, `countBytes` counts its bytes
|
||||
for the byte limits, and once any request but the health check has ended,
|
||||
`countAnomalies` counts it for the anomaly thresholds.
|
||||
- `internal/metrics`: the metrics, counted as the other parts tell it what
|
||||
happened, and served in the Prometheus text format.
|
||||
- `internal/bans`: the ban ledger: each netblock's bans with their notes, how
|
||||
@@ -1696,7 +1808,9 @@ the same refresh, keep fetching within the same hour.
|
||||
- `internal/reputation`: fetches the blocklists and the file
|
||||
`SWWAF_ASN_LIMIT_PERCENT_URL` names as they are due, keeps the last good copy
|
||||
of each, and tells which blocklists list an address and what percentage the
|
||||
file gives an AS number.
|
||||
file gives an AS number; and asks the DNSBL zones about clients in the
|
||||
background, through the standard library's resolver, keeps their verdicts, and
|
||||
tells which zones' verdicts list an address.
|
||||
- `internal/ratelimit`: the table of clients: counts each client's requests and
|
||||
bytes, tells when they take it over a rate limit or a byte limit, and keeps
|
||||
each client's history.
|
||||
@@ -1724,7 +1838,8 @@ the same refresh, keep fetching within the same hour.
|
||||
|
||||
Besides the Go standard library, `github.com/hashicorp/golang-lru/v2` keeps the
|
||||
table of clients to 20,000 and the GeoJS answers to 100,000, dropping the least
|
||||
recently seen, the anomaly counters to 20,000, dropping the one counted least
|
||||
recently seen, the DNSBL zones' verdicts to 100,000, dropping the one fetched
|
||||
longest ago, the anomaly counters to 20,000, dropping the one counted least
|
||||
recently, and the banned netblocks in the order they were last seen, from which
|
||||
the ledger picks the ban to drop past `SWWAF_MAX_BANS`, and
|
||||
`github.com/prometheus/client_golang` keeps the metrics and serves them, and
|
||||
|
||||
Reference in New Issue
Block a user