auth: sign-in limits per address and name; successes free (audit A1, A8, A9)

Security audit 2026-10-17, findings A1 (medium), A8, A9 (low), stage 3.
Under the shipped Apache-on-host deployment with TRUSTED_PROXY_CIDR empty,
every visitor has the Docker gateway's address, and every public limit was
keyed on the address alone: ten POSTs to /login per 15 minutes (any body,
successes counted) locked out every administrator; likewise /login/reset,
the link pages and /setup/. IPv6 was keyed per /128.

Now (internal/web/auth/limitkeys.go):
- sign-in counts per address and username (10 / 15 min) and per address
  alone at 100; a right password refunds both (rateLimiter.Refund);
- a reset request counts per address and username (5) and per address
  alone (50); a link's page per link, by token hash (30), and per address
  alone (300); setup per address as before;
- an IPv6 address is keyed by its /64 (limitAddr);
- proxyWatch: when the first 20 requests after a start all come from one
  private or loopback peer with X-Forwarded-For and no proxy is trusted,
  the log says so once with the TRUSTED_PROXY_CIDR value to set;
- a refused sign-in or a lockout is logged, five lines per address in 15
  minutes, the typed name printable and cut (refusalLog, logName).
The shipped compose cannot know the gateway, so the guide's install step
prints it (docker network inspect) and says to set it.

Tests: TestLoginBehindOneUntrustedProxyKeepsOthersIn (the reader's case),
TestLoginSuccessesDoNotCount, TestIPv6IsLimitedPerSlash64,
TestProxyWatchAndRefusalsAreLogged; the existing lockout tests pass.

Docs: security.md § First-run setup, § Authentication; guide § Full
deployment; CHANGELOG; plan. Also the I1 plan line's Done note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 22:10:47 +03:00
co-authored by Claude Opus 5.5
parent 1bafd78364
commit 3a8e19b71c
9 changed files with 388 additions and 38 deletions
+16 -6
View File
@@ -762,21 +762,31 @@ the argument.
cannot read `/etc/postfix/tls/privkey.pem`; security.md § Output and
process lists the three groups and who is in each. Rollout: `chgrp 10997
/root/selfpost-deploy/certs/privkey.pem` and the same in
`cert-deploy-hook.sh` on the test server. — **Opus**, 2.5 h.
`cert-deploy-hook.sh` on the test server. — **Opus**, 2.5 h. *Done
2026-10-17:* the e2e stage gives its key to 10997 at 0640 when the runner
is root and `testFullBackupRestore` checks both uids; the test server's
key at the rollout.
### Stage 3 — limits and defaults (~7 h)
- [ ] **A1.** (a) Compose and guide set `TRUSTED_PROXY_CIDR` for the
- [x] **A1.** (a) Compose and guide set `TRUSTED_PROXY_CIDR` for the
Apache-on-host scenario (the Docker gateway, with the command that prints
it) and the panel logs a warning at start when the first N requests all
come from one private peer carrying XFF and the list is empty; (b) the
login limiter also keyed by username with its own window, the link-page
limiters by token hash; (c) successful sign-ins not counted. security.md
records (a)-(c). Done when the reader's test passes with two clients
behind one untrusted peer. — **Opus**, 4 h.
- [ ] **A8.** One log line per refused sign-in or lockout, rate-limited per
key. — **Sonnet**, 0.5 h.
- [ ] **A9.** IPv6 keyed by /64. — **Sonnet**, 0.5 h.
behind one untrusted peer. — **Opus**, 4 h. *Done 2026-10-17:* (a) as a
runtime warning (`proxyWatch`) and the guide's install step with the
command - the shipped compose cannot know the gateway, so it is not set
there; (b) per address and username, per link, the address alone at ten
times that; (c) a right password gives its attempt back.
TestLoginBehindOneUntrustedProxyKeepsOthersIn, TestLoginSuccessesDoNotCount,
TestProxyWatchAndRefusalsAreLogged.
- [x] **A8.** One log line per refused sign-in or lockout, rate-limited per
key. — **Sonnet**, 0.5 h. *Done 2026-10-17* (`refusalLog`).
- [x] **A9.** IPv6 keyed by /64. — **Sonnet**, 0.5 h. *Done 2026-10-17:*
TestIPv6IsLimitedPerSlash64.
- [ ] **A3.** After five unsolicited reset links in a day, one mail saying
further requests are ignored for 24 h, then silence. — **Opus**, 1.5 h.
- [ ] **I6.** `/healthz` answered only to loopback and trusted-proxy peers;