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

5.4 KiB

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:

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:

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)
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