8eec6728d45b2b145f7e91ffc9633ab237cc9bed
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
32f6ed626d |
findings of the security review of stages 2-6
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> |
||
|
|
5c5f49bad1 |
signer: MIME headers and Message-ID signed; a long header line logged (audit M6, M8)
Security audit 2026-10-17, M6 and M8 (nits), stage 6. M6: Content-Type, MIME-Version and Message-ID were not in h= (as in OpenDKIM's default list): a relay could change a MIME boundary or the type and the signature still verified. They are signed when present; Message-ID is not over-signed, since cleanup adds one a message lacks after the milters and an absent name in h= would refuse it. M8: a header line over smtp_line_length_limit (998) is broken by Postfix's smtp client after signing and the signature fails silently; only body lines were logged. longHeaderLine names the header, once per message, in the same words. Tests: TestSignLinesCoverTheMIMEHeadersAndLongHeaderLinesAreNamed; the h= expectations gain Message-ID; e2e dkim_shapes expects content-type: in h= (and the receiver's verification over it). Docs: security.md, architecture.md, guide; CHANGELOG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f1d90a2a1b |
go-milter: build it patched not to keep every header
test / test (push) Canceled after 0s
The security documentation's last open item of panel-dkim-signing (the owner's decision, 2026-10-16): go-milter v0.4.1 added every header of a session to a map of its own, emptied only on ABORT, and handed it to Headers - which no milter of the panel reads. Postfix bounds one header, not their number, so a message of nothing but headers up to the message size limit was held whole by the library in the sender check, the signer and the journal milter on 465/587 and in the inbound journal milter on port 25, where strangers send; the signer's 1 MiB refusal came after it was all in. third_party/go-milter is v0.4.1 from the module cache (BSD 2-Clause, LICENSE kept) with session.go changed: CodeHeader hands the header to Header and keeps nothing, CodeEOH hands Headers an empty map. PATCHES.md says what and why, and how to take a new upstream version. go.mod replaces the module with it; the Dockerfile copies third_party before "go mod download". TestTheLibraryKeepsNoHeaders holds it through the library's own client over two messages of one connection. Comments, security.md (the accepted risk becomes a note), development.md's library table, the plan and CHANGELOG follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c2f63d2789 |
panel-dkim-signing: the queue by name, and findings of the reviews
test / test (push) Canceled after 0s
The first rollout of
|
||
|
|
faa3c11ae7 |
panel-dkim-signing stage 2: OpenDKIM removed
test / test (push) Canceled after 0s
The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.
Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.
Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.
Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.
KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.
e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
14de9a05c8 |
panel-dkim-signing stage 1: the panel's DKIM signer, beside OpenDKIM
internal/milter/sign.go: a milter on two sockets (cmd/panel/signer.go): sign.sock for 465/587, the key of the domain the SASL login belongs to (Bindings.DomainOf, the sender_login_maps Postfix reads), and sign-local.sock for the panel's own sendmail(1) mail, the server's name. go-msgauth/dkim v0.7.0, relaxed/simple, OpenDKIM's dkim_should_signhdrs[] (libopendkim/dkim.c:223-246) in message order plus From once more; keys and selectors from the KeyTable the panel writes, a key path outside the key directory refused; strict - whatever cannot be signed is deferred. From the sources: Postfix 3.10.13 milter8.c:2329-2400 hands a folded header \n-separated, so the signer joins its lines with CRLF; inserts are 1-based in cleanup_ins_header() (cleanup_milter.c:1193), so index 0 is the top, where OpenDKIM puts its own (opendkim.c:3673). go-milter v0.4.1 calls no Abort when a connection ends, so a watchdog closes go-msgauth's streaming signer after an hour and a late end of message is deferred. Postfix: smtpd_milters = sender check, signer, OpenDKIM, journal; non_smtpd_milters = the signer's local socket, OpenDKIM - both sign every message until stage 2. disable_mime_output_conversion=yes (default off, mail_params.h:3088): an 8-bit message is no longer re-encoded after it is signed. Tests: unit tests sign and verify folded, tab-led and space-free headers, an added From:, a changed body, two messages on a connection, odd bodies, and five cases deferred. e2e: the sink image has python3-dkim (dkimpy 1.1.8-2) and the stand's DNS; on an application's message and on a notification the two signatures must agree on h=, bh=, c=, s=, a= and each must pass dkimverify. On the server: make e2e exit 0 (714 s), go vet + go test 27 packages ok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |