Files
Christian Heusel 735a38b8c2 fail2ban: add nginx-stream-deny ban action
Add a ban action that writes `deny <ip>;` lines into the nginx stream
deny snippet, so bans actually match real
client traffic behind the proxies.

The action does not reload nginx itself, there instead is a separate
systemd path unit does that with a cooldown period.

The `nginx-limit-req` jail switches to this banaction when
`nginx_stream_deny` is set on the host.

Related-to: https://gitlab.archlinux.org/archlinux/infrastructure/-/work_items/766
Assisted-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Christian Heusel <christian@heusel.eu>
2026-10-07 16:08:34 +02:00

29 lines
1.2 KiB
Django/Jinja

#
# {{ansible_managed}}
#
# Ban action for web servers behind the HAProxy ADN. The regular firewalld
# banaction is useless there: every packet arrives from an allowlisted ADN IP,
# so an ipset ban of the real client IP never matches. Instead we write
# `deny <ip>;` lines into an nginx stream snippet that the L4 front-proxy
# includes (see the nginx role, nginx_stream_deny).
#
# We deliberately do NOT reload nginx here: a systemd path unit
# (nginx-stream-deny-reload) watches the snippet and reloads nginx with a
# cooldown, coalescing a burst of bans into a single reload. nginx `deny`
# accepts IPv4 and IPv6 literally, so no address-family juggling is needed.
[Definition]
denyfile = /etc/nginx/snippets/stream-fail2ban-deny.conf
actionstart = touch "<denyfile>"
actionstop =
actioncheck =
# idempotent append (avoids duplicate deny lines when fail2ban replays active
# bans on reload/restart)
actionban = grep -qxF 'deny <ip>;' "<denyfile>" || echo 'deny <ip>;' >> "<denyfile>"
# rewrite without the banned line via a fixed temp file, then atomically swap.
# no shell variables: fail2ban does not preserve them across the ';' here.
actionunban = grep -vxF 'deny <ip>;' "<denyfile>" > "<denyfile>.tmp"; mv -f "<denyfile>.tmp" "<denyfile>"