check / check (push) Waiting to run
The old README opened with project history and one list of a hundred settings. The new one leads with what smallwebwaf is, what it protects against, how to build and deploy it, how it sits in front of the app, what happens to a request, the settings most deployments touch, how to check it works and how to develop it. The reference material, every setting, bans, rule files, the Core Rule Set, the admin endpoints, the request log, alerts, metrics, state files, lookups, reputation sources, the image and gitea, moves to files under docs/, each linked from the README. Every statement was checked against the code on next. Model: fable-5-1
150 lines
5.9 KiB
Markdown
150 lines
5.9 KiB
Markdown
# Alerts
|
|
|
|
Each alert goes to each destination you name. `SWWAF_ALERT_WEBHOOK_URL` gets it
|
|
as one JSON object. `SWWAF_ALERT_SLACK_WEBHOOK_URL` and `SWWAF_ALERT_NTFY_URL`
|
|
get a message made from it. The settings are under "Alert destinations" in
|
|
[`settings.md`](settings.md). An alert is one of these events, and is sent when
|
|
its event is in `SWWAF_ALERT_EVENTS`:
|
|
|
|
- `ban`: a ban `smallwebwaf` made.
|
|
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
|
|
a request.
|
|
- `waf_block`: the Core Rule Set scored a request at or over the threshold. In
|
|
`block` mode the request was refused. In `detect` mode it was not, and the
|
|
alert's `detail` gives `mode`.
|
|
- `anomaly`: a count over an anomaly threshold.
|
|
- `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.
|
|
One alert per source.
|
|
- `source_failure`: one of:
|
|
- GeoJS failing;
|
|
- a list that cannot be fetched;
|
|
- a DNS blocklist or AbuseIPDB failing or refusing a query;
|
|
- the day's AbuseIPDB checks used up.
|
|
- `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.
|
|
- `summary`: the hourly summary described under "Repeats and the hourly cap"
|
|
below.
|
|
|
|
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
|
|
`detail`.
|
|
|
|
## The JSON object
|
|
|
|
This is the alert for a ban for a broken rate limit, shown indented; it is sent
|
|
on one line:
|
|
|
|
```json
|
|
{
|
|
"instance": "fsn1app1/gitea",
|
|
"time": "2026-10-06T12:00:00.123461Z",
|
|
"event": "ban",
|
|
"client": "203.0.113.9",
|
|
"netblock": "203.0.113.9/32",
|
|
"asn": "",
|
|
"as_name": "",
|
|
"country": "",
|
|
"reason": "requests per minute over the limit of 1000",
|
|
"detail": {
|
|
"ban_expires": "2026-10-06T13:00:00.123Z",
|
|
"cause": "limit",
|
|
"notes": {
|
|
"asn": "",
|
|
"as_name": "",
|
|
"country": "",
|
|
"kind": "requests",
|
|
"limit": 1000,
|
|
"window": "minute",
|
|
"count": 1001,
|
|
"request": {
|
|
"time": "2026-10-06T12:00:00.123456789Z",
|
|
"method": "GET",
|
|
"host": "app.example",
|
|
"path": "/owner/repo/commits/branch/main?page=812",
|
|
"status": 403,
|
|
"user_agent": "scraper/1.0"
|
|
},
|
|
"requests": 5210,
|
|
"refused": 0,
|
|
"earlier_bans": {
|
|
"limit": 0,
|
|
"attack": 0,
|
|
"admin": 0,
|
|
"crowdsec": 0
|
|
}
|
|
}
|
|
},
|
|
"suppressed_repeats": 0
|
|
}
|
|
```
|
|
|
|
- `instance` is `SWWAF_INSTANCE_NAME`. `time` is when the alert was raised.
|
|
`event` is one of the events above.
|
|
- `client` is the address whose request raised the alert. `netblock` is the
|
|
ban's netblock, the netblock an anomaly counted, or the client's group. Both
|
|
are empty for `source_failure` and `file_error`. `asn`, `as_name` and
|
|
`country` are the client's as known when the alert was raised. They are empty
|
|
when GeoJS had not answered yet.
|
|
- `reason` is a short sentence, such as
|
|
`requests per minute over the limit of 1000` or `listed by a blocklist`.
|
|
- `detail` is what is particular to the event:
|
|
- for a ban, its `cause`, `ban_expires` and `notes` (see
|
|
[`bans.md`](bans.md) for the notes);
|
|
- for an anomaly, the `scope`, `window`, `kind`, `count` and `threshold`;
|
|
- for a reputation hit, the `source` and, for AbuseIPDB, the `score`;
|
|
- for a Core Rule Set match, the `rule_ids`, `score`, `method` and `path`;
|
|
- for a source failure, the `source` and `error`;
|
|
- for a file error, the `file` and `error`.
|
|
- `suppressed_repeats` is how many repeats the cooldown held back before this
|
|
alert.
|
|
|
|
## Slack and ntfy
|
|
|
|
Slack and ntfy get a message with a title and a text. The title is the instance
|
|
and the event, such as `fsn1app1/gitea: ban`. The text is the `reason`, then one
|
|
line for each of these that the alert has:
|
|
|
|
- the client, the netblock and the country;
|
|
- the file, the source, the error and the mode from `detail`;
|
|
- the suppressed repeats, if any.
|
|
|
|
Slack gets the title in bold and the text below it, with `&`, `<` and `>`
|
|
escaped. ntfy gets the text as the message, with these headers:
|
|
|
|
- `Title`: the title;
|
|
- `Authorization`: the token, as `Bearer <token>`, while
|
|
`SWWAF_ALERT_NTFY_TOKEN` is set;
|
|
- `Priority` and `Tags`: by event, as listed below.
|
|
|
|
The priority and tag by event:
|
|
|
|
- `ban`: priority `default`, tag `no_entry`.
|
|
- `permanent_ban`: priority `high`, tag `no_entry`.
|
|
- `waf_block`: priority `default`, tag `shield`.
|
|
- `anomaly`: priority `high`, tag `chart_with_upwards_trend`.
|
|
- `reputation_hit`: priority `low`, tag `label`.
|
|
- `source_failure` and `file_error`: priority `high`, tag `warning`.
|
|
- `summary`: priority `default`, tag `bar_chart`.
|
|
|
|
## Repeats and the hourly cap
|
|
|
|
A repeat is an alert for the same event on the same netblock, source, file or
|
|
thing counted, less than `SWWAF_ALERT_COOLDOWN` after the last. It is held back
|
|
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
|
|
by event. Once the hour ends, one alert with the event `summary` gives those
|
|
counts. It also counts the repeats that a cooldown held back and that no later
|
|
alert reported.
|
|
|
|
## Sending
|
|
|
|
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
|
|
than `408` and `429` refuses it, and it is dropped. Anything else is a failure:
|
|
the alert is sent again a second later, and twice as long after each further
|
|
failure, up to a minute. `alerts.json` keeps the queues and cooldowns across a
|
|
restart (see [`state-files.md`](state-files.md)).
|