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.