SPEC: requirements and reasons only, reference material in docs/
check / check (push) Waiting to run
check / check (push) Waiting to run
docs/settings.md is now the one settings reference; the spec keeps the conventions, the off list and each group's requirements and reasons, and links to it for the per-setting entries. The endpoint list, the ban notes, the state file contents, the rule file format and the alert object likewise live in docs/ and are linked from the spec. The package list from the README's Design section joins "Architecture". The nine docs files are byte-identical to those on the README branch. Package comments that named "Alert webhook schema", "Request log" and "Metrics endpoint" for their field lists now name the docs files. Model: fable-5-1
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
# 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)).
|
||||
Reference in New Issue
Block a user