Files
infix/package/webui/default.conf
T
Joachim Wiberg beabc1fe5b webui: rate-limit POST /login via nginx limit_req
Adds an nginx-side rate limit on POST /login so a brute-force or DoS
attempt can't pin the box on the bcrypt-shaped credential check inside
rousette/PAM.  Configuration:

- Keyed on $binary_remote_addr (per source IP), 5 requests/minute
  sustained with a burst of 3 nodelay, a real user fumbling their
  password three times in a row sails through; the 4th attempt in
  the same window returns 429.
- GET /login is unmetered (the keyed map yields the empty string,
  which limit_req treats as "no key, no limit"), so a 401-driven
  HX-Redirect bouncing the user back to the login page doesn't
  eat into the budget.
- 32k zone (~500 IPs, the minimum nginx accepts).  At cache
  saturation an attacker rotating addresses still tops out around
  ~2500 attempts/min, all serialised through bcrypt one at a time.
- 429 instead of the default 503 keeps the response out of the
  /50x.html error_page rewrite (which auto-refreshes and would
  loop a throttled client straight back to /login).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-15 19:45:20 +02:00

45 lines
1.4 KiB
Plaintext

# 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;
}