The review of 4433688..0808779 (read-only, a separate agent) found one
medium and some lows; all fixed here.
Medium, proven by the reviewer: A1's userKey put the typed username into
the limiter map whole, and the public forms had no body bound beyond
net/http's 10 MiB for a parsed form - each refused request kept up to
10 MiB in memory for fifteen minutes. userKey now holds a 96-bit hash of
the folded name, and sign-in, reset, a reset link's page and setup read at
most 64 KiB (limitForm, http.MaxBytesReader).
TestPublicFormsAreBoundedAndKeysShort.
Lows:
- Refund skipped the request that filled a bucket (count < max), so a
right password at the ceiling left the shared address blocked; Refund
now always undoes the Allow it follows.
- The reset form did not give the address its attempt back when the
name's bucket refused (sign-in did); it does.
- A3's day of five links is spent only by a link that goes out, and the
price of the pause - anyone knowing a username can pause that owner's
reset for a day - is recorded in security.md.
- longHeaderLine counted a trailing CR on a header's first line.
- test.yml declares permissions: contents: read.
Not changed, as the review's nits: the store matches names exactly while
the limiter folds them (stricter, said in the comment now); a session from
a longer window is honoured until its next Touch (the next request).
go test ./internal/web/auth ./internal/milter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security audit 2026-10-17, findings W6/M9 (low), stage 4. Every
application, upstream, recipient or report-address save reloaded Postfix
through supervisord, and POST .../dns-recheck bypassed the five-minute
cache for up to ~14 resolver queries - both at whatever rate an
authenticated user pressed.
- writeChanged: each generated map is written only when its content
differs from the file on disk, and Postfix is reloaded only when one was
(sender map, inbound maps, DMARC maps). Health's Reload configuration
ends with an explicit reload (app.Service.ReloadPostfix, SenderMaps
gains Reload), so it still re-reads whatever is on disk. No debounce:
with unchanged saves gone, a burst is real changes Postfix must see.
- auth.AllowDNSRecheck: twenty forced re-checks in 15 minutes per user;
past that the outbound, inbound and settings re-check show the cached
result without the "re-checked" flash, and the log says why.
Tests: TestARebuildThatChangesNothingDoesNotReload,
TestDNSRechecksArePerUser; the fakes gain Reload. Docs: guide § Overview
and Health; CHANGELOG; plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>