Trust the RFC 1918 ranges as proxies when TRUSTED_PROXIES is unset (closes #333)

Unset or empty, TRUSTED_PROXIES now defaults to 10.0.0.0/8,
172.16.0.0/12 and 192.168.0.0/16, so a reverse proxy reaching the app
over a Docker network or a private LAN gets per-client rate-limit
buckets without configuration. A set value replaces the default; an
unparseable one still fails startup.

The startup warning for an empty list goes, with its test hook and
test, since the default is no longer empty. The README's
configuration table, Trusted proxies, upaas and reverse-proxy sections
describe the new default and when to narrow it to the proxy alone.

Model: opus-5-5
This commit is contained in:
2026-09-29 08:58:10 +00:00
parent f0adeafde3
commit b12e204d1f
10 changed files with 126 additions and 257 deletions
+4 -3
View File
@@ -103,9 +103,10 @@ func (h *Handlers) renderLoginError(
// The credential check runs BEFORE any rate-limit budget is
// consulted, and only a failed check spends budget. That is what
// keeps the single administrative path reachable: behind the reverse
// proxy this deployment requires, with TRUSTED_PROXIES unset, every
// client shares one bucket, so a limiter spent on arrival lets any
// stranger deny the operator's own correct password indefinitely.
// proxy this deployment requires, when TRUSTED_PROXIES does not cover
// it, every client shares one bucket, so a limiter spent on arrival
// lets any stranger deny the operator's own correct password
// indefinitely.
//
// Verifying first means every login POST costs an Argon2id hash, so
// the work is taken under a bounded number of verification slots.
+6 -6
View File
@@ -25,7 +25,7 @@ const (
// sharedProxyPeer is the whole point of this file. Production is
// required to run behind a TLS-terminating reverse proxy, and
// TRUSTED_PROXIES defaults to empty, so every client — attacker
// when TRUSTED_PROXIES does not cover it every client — attacker
// and operator alike — reaches the process from the proxy's
// address and shares one rate-limit bucket. Both parties in
// these tests therefore use the same RemoteAddr.
@@ -115,11 +115,11 @@ func floodFailures(
// done-criterion of https://git.eeqj.de/sneak/webhooker/issues/150.
//
// The attacker and the operator share one rate-limit bucket, because
// behind the mandated reverse proxy with TRUSTED_PROXIES unset every
// client keys on the proxy's address. The attacker floods the
// operator's own username — a single-admin product has a predictable
// one — far past the failure limit. The operator must still be able
// to log in with the correct password.
// behind the mandated reverse proxy, when TRUSTED_PROXIES does not
// cover it, every client keys on the proxy's address. The attacker
// floods the operator's own username — a single-admin product has a
// predictable one — far past the failure limit. The operator must
// still be able to log in with the correct password.
//
// This fails if credentials stop being verified ahead of the limiter.
func TestLogin_StrangersFloodCannotLockOutTheOperator(t *testing.T) {