# Throttle POST /login so a brute-force attempt can't pin the box on # the bcrypt-shaped credential check inside rousette/PAM. GET is left # unmetered: page loads after a 401-driven HX-Redirect shouldn't eat # into the budget. # # Zone is 32k (~500 IPs) — the minimum nginx accepts via shared-memory # allocation. With the cache full an attacker rotating source addresses # still tops out at ~2500 attempts/min total, all serialised through # bcrypt one at a time. # # 429 instead of the default 503 keeps the rate-limit response out of # the /50x.html error_page rewrite below (which auto-refreshes — would # loop a throttled client straight back to /login). map $request_method $webui_login_key { POST $binary_remote_addr; default ""; } limit_req_zone $webui_login_key zone=webui_login:32k rate=5r/m; limit_req_status 429; server { listen 80; listen [::]:80; server_name _; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name _; include ssl.conf; # 404 also points at /50x.html: the page is a "Loading…" screen # with a meta-refresh, so the early-boot window where the Go # backend isn't up yet (and any other transient 404 / 5xx) self- # recovers as soon as upstream comes back. error_page 404 500 502 503 504 /50x.html; location = /50x.html { root html; } include /etc/nginx/app/*.conf; }