Fail2ban watches the nginx access log and auto-bans IPs that hit 10 404s in 60 seconds for 24 hours. nginx runs as a Docker container managed by Pacemaker, so the log must be mounted as a host volume for fail2ban to read it as plain text.
Fail2ban is managed as a Pacemaker resource — not a standalone systemd service. It runs only on the active node alongside nginx, and fails over with it automatically.
nginx-reverse-proxy Docker container runningfirewalld running (already enabled on thing-1/thing-2)Run identically on both thing-1 and thing-2:
sudo dnf install fail2ban -y
Do NOT run
systemctl enableorsystemctl start— Pacemaker owns the lifecycle. Enabling fail2ban in systemd will cause both nodes to run it simultaneously after a reboot, fighting over the same log file.
Edit the nginx compose file (docker-compose.yml in the nginx-proxy directory) and add the log volume:
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:rw
- /mnt/ha-shared/letsencrypt:/etc/letsencrypt:ro
- /mnt/ha-shared/acme-challenge:/var/www/acme-challenge:rw
- /var/log/nginx:/var/log/nginx:rw
Verify nginx.conf has logging configured (should already be present):
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
Restart nginx via Pacemaker to apply the new volume mount:
pcs resource restart ha-nginx-proxy
Verify the log file exists on the host:
ls -la /var/log/nginx/access.log
sudo vim /etc/fail2ban/filter.d/nginx-badbots.conf
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*" 404
ignoreregex =
sudo vim /etc/fail2ban/jail.d/nginx.conf
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 60
bantime = 86400
backend = auto
maxretry = 10 — 10 404s triggers a banfindtime = 60 — within a 60 second windowbantime = 86400 — banned for 24 hoursbackend = auto — uses pyinotify for log watchingsudo pcs resource create ha-fail2ban systemd:fail2ban \
op monitor interval=30s
sudo pcs constraint order ha-nfs-mount then ha-fail2ban
sudo pcs constraint order ha-nginx-proxy then ha-fail2ban
sudo pcs constraint colocation add ha-fail2ban with ha-nginx-proxy score=INFINITY
This enforces the full dependency chain:
ha-nfs-mount → ha-nginx-proxy → ha-fail2ban
NFS mounts first, nginx starts and begins writing logs, fail2ban starts and begins watching them.
Verify:
sudo pcs status
sudo pcs constraint show
sudo fail2ban-client status nginx-badbots
Expected output:
Status for the jail: nginx-badbots
|- Filter
| |- Currently failed: X
| |- Total failed: X
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Watch live activity:
sudo tail -f /var/log/fail2ban.log
| Command | Purpose |
|---|---|
sudo fail2ban-client status nginx-badbots |
Check jail status and banned IPs |
sudo fail2ban-client set nginx-badbots unbanip <IP> |
Manually unban an IP |
sudo fail2ban-client set nginx-badbots banip <IP> |
Manually ban an IP |
sudo tail -f /var/log/fail2ban.log |
Watch live ban activity |
sudo systemctl restart fail2ban |
Restart after config changes (active node only) |
sudo pcs resource restart ha-fail2ban |
Restart via Pacemaker |
systemctl enable fail2ban — Pacemaker manages start/stopfirewallcmd-rich-rules by default since firewalld is runningiptables-multiport action — firewalld manages rules on this cluster/var/log/nginx/access.log is a local host path on whichever node is active — not NFS