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:
+18
-4
@@ -89,7 +89,8 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
- Token ≥128 bits (`crypto/rand`); mirrored to `/data/setup-token`.
|
||||
- Token lifetime — **10 minutes**; after expiry, or after a restart with setup
|
||||
unfinished, it is regenerated and logged again.
|
||||
- Rate limiting on `/setup/<token>` per IP, separate from login.
|
||||
- Rate limiting on `/setup/<token>` per client address (an IPv6 one per
|
||||
/64), separate from login.
|
||||
- Token comparison is **constant-time** (`subtle.ConstantTimeCompare`).
|
||||
- Failed attempts do **not** invalidate the token early (protects setup from
|
||||
being DoS-ed).
|
||||
@@ -123,7 +124,18 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
|
||||
### Authentication and sessions
|
||||
|
||||
- Rate limiting on login (per IP, with lockout or delay). The Account form's
|
||||
- Rate limiting on login: ten failures in 15 minutes per client address and
|
||||
username, and a hundred per address alone; a right password gives its
|
||||
attempt back, so only failures count. An IPv6 client is one address per
|
||||
/64. Behind a reverse proxy the panel does not trust (`TRUSTED_PROXY_CIDR`
|
||||
empty) every visitor has the proxy's address: a stranger there can then
|
||||
lock one name, or spray a hundred, but not lock everyone out at the
|
||||
per-name size (until `2.14.4` the one key was the address, successes
|
||||
counted, and ten requests of any body locked every administrator out -
|
||||
security audit 2026-10-17, A1, A9). When the first requests after a start
|
||||
all come from one private peer with `X-Forwarded-For` and no proxy is
|
||||
trusted, the log says so with the value to set. A refused sign-in and a
|
||||
lockout are logged, five lines per address in 15 minutes (A8). The Account form's
|
||||
current-password check has a limit of its own, ten in 15 minutes per user,
|
||||
so guesses there do not lock the address out of signing in (`2.5.4`).
|
||||
- An unknown username is refused in the time a wrong password is: the sign-in
|
||||
@@ -163,8 +175,10 @@ The panel is exposed to the internet — the items below are **not optional**.
|
||||
token. The request form answers the same for every name and sends the mail
|
||||
after answering, so neither the words nor the time tell whether a name
|
||||
exists or is confirmed; requests are limited to five in 15 minutes per
|
||||
client address, mails to five an hour per account, and a link's page to 30
|
||||
requests in 15 minutes per client address. A link's page changes nothing
|
||||
client address and username (fifty per address alone), mails to five an
|
||||
hour per account, and a link's page to 30 requests in 15 minutes per link
|
||||
(300 per address alone), so one stranger behind a shared proxy cannot stop
|
||||
the others asking for or opening a link (`2.14.4`; audit A1). A link's page changes nothing
|
||||
on `GET` (a mail scanner opens links): the button does. Setting a password
|
||||
from a link ends every session of the account and every other reset link.
|
||||
Changing the account e-mail asks for the current password, as the password
|
||||
|
||||
Reference in New Issue
Block a user