fail2ban on the application log, not just the server log

SSH and HTTP basic auth are covered by shipped filters. A brute force against the application’s own login form is invisible to both, because it is a 200 response to a POST that nginx has no opinion about.

# /etc/fail2ban/filter.d/shop-login.conf
[Definition]
failregex = ^.*"event":"auth.failed".*"ip":"<HOST>".*$

# jail.local
[shop-login]
enabled  = true
filter   = shop-login
logpath  = /var/www/shop/storage/logs/app.json
maxretry = 5
findtime = 300
bantime  = 3600

This is the payoff for structured logging: the application emits a field for the client address and the filter matches on it, rather than a regex over prose. Behind a proxy the logged address must be the real client, which means the application has to trust and read the forwarded header — otherwise the jail bans the load balancer and takes the site down.