AbuseIPDB scores for clients that broke a limit, within a daily budget (closes #105)
check / check (push) Waiting to run
check / check (push) Waiting to run
With SWWAF_ABUSEIPDB_KEY set, a client whose history counts an offence is checked in the background, at most SWWAF_ABUSEIPDB_DAILY_BUDGET checks a day, the count kept in reputation.json. A score at or over SWWAF_ABUSEIPDB_MIN_SCORE is a hit for SWWAF_REPUTATION_ACTION, logged as abuseipdb and alerted with its score. A failure or the used-up budget gives no score and raises source_failure. The key goes only in the Key header. Judgement call: the budget's day is UTC; AbuseIPDB documents no reset time. Judgement call: each check sent spends budget; a minute's pause after a failure. Judgement call: offences are those the history counts, so far broken limits only. Model: opus-5-5
This commit is contained in:
@@ -26,36 +26,39 @@ 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 are the first two parts of the stage
|
||||
unusual traffic that refuse nothing. So are the first three 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).
|
||||
file of AS numbers' percentages fetched the same way, the DNS blocklists (DNSBL
|
||||
zones), which it asks about each client in the background, and AbuseIPDB, which
|
||||
it asks in the background about each client that has broken a limit.
|
||||
`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, or AbuseIPDB scores at or over the score you set,
|
||||
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, the DNSBL zones' verdicts, AbuseIPDB's scores and the AbuseIPDB
|
||||
checks spent today 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, a DNSBL zone or
|
||||
AbuseIPDB lists, for GeoJS failing, a list it cannot fetch, a DNSBL zone or
|
||||
AbuseIPDB that fails or refuses a query and the day's AbuseIPDB checks used up,
|
||||
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
|
||||
|
||||
@@ -222,35 +225,45 @@ in `bin/state` unless `SWWAF_STATE_DIR` is set, and the default rule file of
|
||||
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 with AbuseIPDB while `SWWAF_ABUSEIPDB_KEY` is
|
||||
set, after the DNSBL zones and before the rate limits (see "AbuseIPDB" below),
|
||||
by the scores it keeps. Only a client whose history counts an offence is
|
||||
checked, so far one that has broken a rate limit or a byte limit, and only in
|
||||
the background, so that no request waits for AbuseIPDB. A score at or over
|
||||
`SWWAF_ABUSEIPDB_MIN_SCORE` is a hit, and `SWWAF_REPUTATION_ACTION` does with
|
||||
its client what it does with one a DNSBL zone's verdict lists. The request's
|
||||
log line names AbuseIPDB, and it raises an alert. A client a blocklist or a
|
||||
DNSBL zone 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 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.
|
||||
zones, AbuseIPDB, 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 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.
|
||||
verdict, AbuseIPDB's score, 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
|
||||
@@ -270,14 +283,15 @@ 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 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).
|
||||
anomaly threshold, for a client a blocklist, a DNSBL zone or AbuseIPDB lists,
|
||||
for GeoJS failing, a list it cannot fetch, a DNSBL zone or AbuseIPDB that
|
||||
fails or refuses a query, and the day's AbuseIPDB checks used up, 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
|
||||
@@ -340,8 +354,9 @@ 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 DNSBL zones, 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, AbuseIPDB, 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.
|
||||
@@ -457,21 +472,28 @@ effective settings are logged at start.
|
||||
`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_ABUSEIPDB_KEY` (default unset): the key of your AbuseIPDB account,
|
||||
which clients are checked with (see "AbuseIPDB" below). While it is unset, no
|
||||
client is checked. The settings logged at start show `********` in its place.
|
||||
- `SWWAF_ABUSEIPDB_MIN_SCORE` (default `75`): the least abuse confidence score,
|
||||
from 0 to 100, that is a hit.
|
||||
- `SWWAF_ABUSEIPDB_DAILY_BUDGET` (default `900`): the most checks made in a day,
|
||||
in UTC, a whole number above zero. AbuseIPDB's free accounts may make 1,000.
|
||||
- `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.
|
||||
zone's verdict lists, or whose AbuseIPDB score is a hit, as
|
||||
`SWWAF_BLOCKLIST_ACTION` is for a blocklist: `deny`, `limit:<percent>` or
|
||||
`log`. Such a verdict is less certain than a list such as DROP, so by default
|
||||
its client 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.
|
||||
client, or AbuseIPDB's score of it, is used after it was given.
|
||||
- `SWWAF_REPUTATION_TIMEOUT` (default `2s`): how long a query to a zone, or a
|
||||
check with AbuseIPDB, 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, 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
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny` or is listed by a DNSBL zone or AbuseIPDB
|
||||
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`.
|
||||
- `SWWAF_LIMIT_BAN_DURATION` (default `1h`): the ban for a first broken rate
|
||||
@@ -616,7 +638,8 @@ 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`, `SWWAF_REPUTATION_CACHE_TTL`,
|
||||
`SWWAF_BLOCKLIST_REFRESH`, `SWWAF_ABUSEIPDB_MIN_SCORE`,
|
||||
`SWWAF_ABUSEIPDB_DAILY_BUDGET`, `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.
|
||||
@@ -624,8 +647,9 @@ 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, 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.
|
||||
anomaly counters, at most 100,000 verdicts of the DNSBL zones, with at most
|
||||
1,000 queries to them under way at once, and at most 100,000 scores of
|
||||
AbuseIPDB.
|
||||
|
||||
### Settings given as files
|
||||
|
||||
@@ -709,30 +733,31 @@ which every line has.
|
||||
- `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`, 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
|
||||
`block` rule refused, `too_large` for a request or response over its size
|
||||
limit, `timed_out` for one that ran out of time, `upstream_error` when the app
|
||||
could not be reached or its answer broke off, and `admin` for one
|
||||
`smallwebwaf` answered at its own endpoint.
|
||||
`SWWAF_BLOCKLIST_ACTION` is `deny`, or listed by a DNSBL zone or AbuseIPDB
|
||||
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 `block` rule refused, `too_large` for a request or
|
||||
response over its size limit, `timed_out` for one that ran out of time,
|
||||
`upstream_error` when the app 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 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.
|
||||
verdict, AbuseIPDB's score, 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, `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.
|
||||
`SWWAF_REPUTATION_ACTION` for a DNSBL zone whose verdict lists it or an
|
||||
AbuseIPDB score that is a hit, 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.
|
||||
- `counts` gives the client's requests in the minute, the hour and the day as
|
||||
the rate limits count them, this request included: in each window, those in
|
||||
the bucket under way and a share of those in the bucket before, so a count can
|
||||
@@ -741,11 +766,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, 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.
|
||||
ban, the country lists, a blocklist, a DNSBL zone's verdict or AbuseIPDB's
|
||||
score 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
|
||||
@@ -756,15 +781,17 @@ which every line has.
|
||||
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 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.
|
||||
verdict lists, or whose AbuseIPDB score is a hit, 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,
|
||||
then `abuseipdb`, 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, nor
|
||||
is AbuseIPDB for such a score. 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, nor AbuseIPDB's score for
|
||||
one a blocklist or a zone 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`.
|
||||
@@ -835,12 +862,14 @@ it, as below. An alert is for one of these events, and is sent when
|
||||
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 or a DNSBL zone's verdict
|
||||
lists, one alert for each blocklist and each zone that lists it, whatever
|
||||
lists, or whose AbuseIPDB score is a hit, one alert for each blocklist and
|
||||
each zone that lists it, and one for AbuseIPDB, 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).
|
||||
failing (see "Blocklists" below), a query to a DNSBL zone failing or refused
|
||||
(see "DNS blocklists" below), or a check with AbuseIPDB failing or refused, or
|
||||
the check that uses up the day's AbuseIPDB checks (see "AbuseIPDB" 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
|
||||
@@ -913,7 +942,8 @@ 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` or `listed by a DNSBL zone`.
|
||||
for a `reputation_hit`, `listed by a blocklist`, `listed by a DNSBL zone` or
|
||||
`scored by AbuseIPDB at or over SWWAF_ABUSEIPDB_MIN_SCORE`.
|
||||
- `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`,
|
||||
@@ -921,11 +951,12 @@ 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 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.
|
||||
the `source`, the URL of the blocklist, the zone or `abuseipdb`, and for
|
||||
AbuseIPDB the `score`; for `source_failure`, the `source`, `geojs`, the URL of
|
||||
the list, the zone or `abuseipdb`, 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).
|
||||
|
||||
@@ -971,14 +1002,14 @@ 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 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.
|
||||
a `reputation_hit` about the same blocklist, zone or AbuseIPDB, 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.
|
||||
|
||||
Past `SWWAF_ALERT_MAX_PER_HOUR` alerts in an hour, the hour's other alerts are
|
||||
held back and counted by event. An alert held back this way starts no cooldown.
|
||||
@@ -1043,8 +1074,12 @@ with times in UTC.
|
||||
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.
|
||||
zone gave it, `fetched`; and under `abuseipdb` (see "AbuseIPDB" below), the
|
||||
`day`, in UTC, whose checks it counts, left out before the first, the checks
|
||||
`spent` that day, and under `scores`, each score of AbuseIPDB still in use:
|
||||
the `client`'s address, its `score`, and when AbuseIPDB 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`,
|
||||
@@ -1298,6 +1333,13 @@ scraped, and keeps this one as `exported_instance` unless the scrape sets
|
||||
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.
|
||||
- While `SWWAF_ABUSEIPDB_KEY` is set, by `source`, `abuseipdb`:
|
||||
`smallwebwaf_reputation_hits_total`: the requests whose client's AbuseIPDB
|
||||
score is a hit, a series that comes with the first;
|
||||
`smallwebwaf_reputation_queries_total`: the checks made, each of which spends
|
||||
one of the day's budget; `smallwebwaf_reputation_failures_total`: those that
|
||||
failed; and `smallwebwaf_reputation_daily_budget_remaining`: the checks the
|
||||
day's `SWWAF_ABUSEIPDB_DAILY_BUDGET` has left.
|
||||
- `smallwebwaf_tracked_clients`: the clients in the table of clients.
|
||||
- `smallwebwaf_state_file_writes_total`,
|
||||
`smallwebwaf_state_file_write_failures_total`,
|
||||
@@ -1490,8 +1532,9 @@ goes through the candidates one by one.
|
||||
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, 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.
|
||||
verdicts of the DNSBL zones, AbuseIPDB's scores 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
|
||||
@@ -1771,6 +1814,53 @@ 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.
|
||||
|
||||
## AbuseIPDB
|
||||
|
||||
While `SWWAF_ABUSEIPDB_KEY` holds the key of an AbuseIPDB account, `smallwebwaf`
|
||||
asks AbuseIPDB's check endpoint, `https://api.abuseipdb.com/api/v2/check`, for
|
||||
the abuse confidence score of a client's own address, from 0 to 100. It is unset
|
||||
by default, for the reason no blocklist is named, and since AbuseIPDB needs an
|
||||
account. An IPv6 client is checked by its own address, not by its /64.
|
||||
|
||||
Only a client whose history counts an offence is checked, so that the checks are
|
||||
spent on suspects: so far, one that has broken a rate limit or a byte limit. A
|
||||
client dropped from the table of clients loses its history, and with it its
|
||||
offences. A client is checked in the background, at its first request after its
|
||||
offence that reaches the check: no request waits, a request refused under its
|
||||
ban is not checked, and the request that has it checked, and any other from it
|
||||
before the answer comes, goes on as from a client without a score.
|
||||
|
||||
A score at or over `SWWAF_ABUSEIPDB_MIN_SCORE`, 75 by default, is a hit, and
|
||||
`SWWAF_REPUTATION_ACTION` says what is done with its client, as for a DNSBL
|
||||
zone's verdict (see "What it does so far" above). Each score, a hit or not, is
|
||||
kept, in memory and in `reputation.json` (see "State files" above), and used for
|
||||
`SWWAF_REPUTATION_CACHE_TTL` after AbuseIPDB gave it, 24 hours by default. The
|
||||
client's first request after that has it checked again, if its history still
|
||||
counts an offence. At most 100,000 scores are kept, the one fetched longest ago
|
||||
dropped first.
|
||||
|
||||
At most `SWWAF_ABUSEIPDB_DAILY_BUDGET` checks are made in a day, 900 by default,
|
||||
below the 1,000 of AbuseIPDB's free accounts. The day is counted in UTC, from
|
||||
00:00. Each check sent spends one of them, whatever AbuseIPDB answers, and the
|
||||
checks spent today are kept in `reputation.json`, so that a restart does not
|
||||
make the budget whole again. The check that spends the last of the day's budget
|
||||
is logged and raised as a `source_failure` alert; from then until the day ends,
|
||||
no client is checked, and a client without a score counts as one AbuseIPDB does
|
||||
not list.
|
||||
|
||||
A check fails when AbuseIPDB answers other than `200`, such as `429` past its
|
||||
own daily limit or `401` for a wrong key, when its answer gives no
|
||||
`abuseConfidenceScore`, and when it does not answer within
|
||||
`SWWAF_REPUTATION_TIMEOUT`. A failure gives no score, and is counted, logged and
|
||||
raised as a `source_failure` alert, held back as a repeat within
|
||||
`SWWAF_ALERT_COOLDOWN`, and no client is checked for a minute after it.
|
||||
|
||||
The key is sent to AbuseIPDB in the `Key` header, and nowhere else. The settings
|
||||
logged at start show `********` in its place, and neither the log, the alerts,
|
||||
the metrics nor `reputation.json` hold it. Given as a file, with
|
||||
`SWWAF_ABUSEIPDB_KEY_FILE`, it can be kept out of the app's reach (see "Settings
|
||||
given as files" above).
|
||||
|
||||
## How the code is laid out
|
||||
|
||||
- `cmd/smallwebwaf`: the binary, which only calls `internal/smallwebwaf`.
|
||||
@@ -1785,14 +1875,14 @@ names another resolver to ask through.
|
||||
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 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.
|
||||
AbuseIPDB's score, 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
|
||||
@@ -1808,9 +1898,11 @@ names another resolver to ask through.
|
||||
- `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; 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.
|
||||
file gives an AS number; 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; and checks clients with AbuseIPDB in the
|
||||
background, keeps their scores and the checks spent today, and tells whether
|
||||
an address's score is a hit.
|
||||
- `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.
|
||||
@@ -1838,11 +1930,11 @@ names another resolver to ask through.
|
||||
|
||||
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 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
|
||||
recently seen, the DNSBL zones' verdicts and AbuseIPDB's scores to 100,000 each,
|
||||
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
|
||||
`github.com/fsnotify/fsnotify` tells `smallwebwaf` when a state file or a rule
|
||||
file is saved, or the lookup database replaced, and
|
||||
`github.com/oschwald/maxminddb-golang/v2` reads the lookup database, which the
|
||||
|
||||
Reference in New Issue
Block a user