Files
guard/docs/configuration.md
T
abrown38andClaude Opus 5.5 d06bf8d531 Guard: watch logs, ban misbehaving addresses in Housecarl
Files and the journal, patterns with one <IP>, bounties within a window,
bans in a Housecarl ban list with repeat offenders' bans growing; presets
for sshd, jarl, housecarl, steward, bind-xfr, postfix-sasl, dovecot,
nginx-auth and housecarl-drops; guard run/check/test/status/bans/ban/unban;
a Debian package with a sandboxed unit; an end-to-end test with Housecarl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:29:37 +01:00

127 lines
5.4 KiB
Markdown

# Configuration
`/etc/guard/guard.yaml` is Guard's whole configuration. `guard config
--example` prints a commented one. Unknown settings are an error;
`guard check` checks the file, compiles every pattern and asks Housecarl
for the ban lists. Guard reads it when it starts:
`systemctl restart guard` after a change.
## housecarl
| Setting | | |
|---|---|---|
| `url` | | Housecarl's API: `https://router.lan:8070` |
| `socket` | | or its local socket, on the firewall: `/run/housecarl/housecarl.sock` |
| `token_file` (or `token`) | | the API token: one for the ban lists Guard bans into (`bans:write:LIST`) |
| `ca_cert` | | a PEM file with the CA of Housecarl's certificate, if private |
| `insecure` | `false` | don't check Housecarl's certificate |
| `list` | | the ban list a watch bans into unless it names another |
## never_ban
Addresses, prefixes and host names never banned, whatever the logs say:
```yaml
never_ban: [198.51.100.7, 2001:db8:ffff::/48, vps.example.org]
```
Host names are looked up when Guard starts and every ten minutes; if a
lookup fails, the last addresses are kept. Loopback, link-local and
multicast addresses are never banned either. Housecarl has its own list
too: it never bans the inside networks, the firewall's own addresses,
the machine asking, or a ban list's `never`.
## defaults
| Setting | Default | |
|---|---|---|
| `hits` | 5 | an address is banned once it matches this many times… |
| `within` | 10m | …within this long |
| `ban` | 1h | how long a first ban lasts |
| `repeat.factor` | 2 | each earlier ban of the address multiplies the time by this (1: bans don't grow) |
| `repeat.max` | 1w | the longest ban |
| `repeat.forget` | 1w | earlier bans older than this don't count |
Times are written `30s`, `10m`, `1h`, `1d`, `1w`, or combined (`1d12h`).
Housecarl cuts a ban longer than its list's longest.
## watch
Each watch is a log, its patterns, and what a ban is:
```yaml
watch:
- name: ssh
preset: sshd
ban: {protocol: tcp, port: 22}
- name: web-login
file: /var/log/nginx/access.log
match:
- '^<IP> \S+ \S+ \[[^\]]+\] "POST /login[^"]*" 401 '
ignore: ['/healthz']
hits: 10
within: 5m
ban: {list: web, time: 6h}
```
| Setting | | |
|---|---|---|
| `name` | | the watch's name: letters, digits, `-`, `_`, `.` |
| `preset` | | built-in patterns and their usual source ([presets.md](presets.md)) |
| `file` | | a log file, or a glob (`/var/log/nginx/*access.log`) |
| `journal` | | `{unit: ssh.service}` and/or `{identifier: sshd}`; each a name or a list. Entries of any of the units and of any of the identifiers |
| `source` | | `housecarl-drops`: Housecarl's log of dropped packets |
| `match` | | patterns (Go regular expressions) with one `<IP>` each; added to a preset's |
| `ignore` | | lines matching any of these are skipped, even when they match |
| `hits`, `within` | defaults | |
| `ban.list` | `housecarl.list` | the ban list |
| `ban.protocol`, `ban.port` | every port | `tcp` or `udp`, and a port or service name, or a list (`[http, https]`) |
| `ban.time` | `defaults.ban` | a first ban's length |
| `protocol` | `tcp` | `housecarl-drops` only: `tcp`, `udp` or `any`. UDP sources can be faked, so a scanner could get someone else banned |
| `shadow` | `false` | `housecarl-drops` only: count what shadow mode would have dropped |
| `disabled` | `false` | skip the watch |
A watch has one source: `file`, `journal` or `source`. A preset brings
its own, which a `file` or `journal` given replaces.
### How lines are read
* **Files** are followed from their end when Guard starts: only new
lines count. A file that appears later is read from its start. A
rotated file (renamed, and a new one made) is read to its end, then
the new one from its start; a truncated one (logrotate's
`copytruncate`) is noticed while it is shorter than what was read, and
read again from its start. Each line counts at the time Guard reads
it: Guard parses no dates.
* **The journal** is read through `journalctl --follow`, from now on,
with each entry's own time. If journalctl ends, Guard starts it again
after the last entry it saw. The guard user needs the
`systemd-journal` group (the package's unit gives it).
* **Housecarl's log** is read every two seconds (`GET /log`): each
packet a logged rule (or a policy, with `log_policy_drops`) dropped or
rejected counts once. The token needs `log:read`.
### Bounties and bans
Each match adds one to the address's bounty in that watch; matches older
than `within` drop out. When the bounty reaches `hits`, Guard bans the
address and its bounty starts again from nothing. IPv6 addresses are
counted, and banned, by the prefix the ban list says (its `group_v6`,
/64 by default), so an attacker can't step around a ban inside their
/64.
A ban goes to Housecarl at once. If Housecarl can't be reached, it waits
(in the state file, so a restart doesn't lose it) and is sent again
every 30 seconds, for the time it has left. A ban Housecarl refuses (an
address it never bans, a list the token doesn't cover) isn't sent again:
`guard status` and the log say why.
## state, socket, log
| Setting | Default | |
|---|---|---|
| `state` | `/var/lib/guard/state.json` | repeat offenders, recent bans, bans waiting for Housecarl |
| `socket` | `/run/guard/guard.sock` | the local socket `guard status` asks; root and the guard user |
| `log.level` | `info` | `debug` also logs every match with its bounty |
| `log.format` | `text` | or `json` |