docs/own-mta-builds-2026-10-15.md weighs patched builds of Postfix and OpenDKIM against what the panel works around. No Postfix limit needs a patch (the panel reads it through the interfaces it offers); every limit worked around is OpenDKIM's, a project with no release since 2018, three 2026 CVEs open in every Debian suite and no one answering (read in dev/upstream: dkim_process_set runs on a tenant's DKIM-Signature header in signing mode too). Bookworm is in LTS since 2026-07-12 and its Postfix 3.7.11 lacks the June, August and September 2026 upstream security batches, which carry no CVE ids. Proposed as candidates: port25-bare-newline (smtpd_forbid_bare_newline defaults to no on 3.7; our smtp/inet block does not set it), base-trixie (postfix 3.10.13; 331 citation lines to re-read), panel-dkim-signing (go-msgauth/dkim in the panel's milter; OpenDKIM, the syslog relay and the parser port removed, about 1,500 lines). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
18 KiB
Own builds of Postfix and OpenDKIM - the analysis of 2026-10-15
Question (owner, 2026-10-15; roadmap own-mta-builds): is it worth patching Postfix and OpenDKIM and shipping our own builds, to optimise something in SelfPost or to lift a limit we work around?
Answer: no to carrying patched builds of either - nothing we work around needs a patch, and a patch set makes one maintainer the security team of two MTAs. The question does point at two real problems, each with a cheaper answer:
- The image's base is out of date.
debian:bookworm-slimleft regular security support on 2026-07-12; its Postfix3.7.11is code of mid-2024, and the three Postfix security batches of 2026 (June, August, September) carry no CVE ids, so nothing will bring them to bookworm's LTS. The answer is Debian trixie (postfix 3.10.13, the August batch included), not a build of our own: proposed as roadmap row base-trixie. - OpenDKIM has no upstream left. The package is a beta of 2018; three CVEs of 2026 are open in every Debian suite with "the vendor did not respond"; Debian's bug on them has had no answer since 2026-10-03. Every limit we have worked around is OpenDKIM's. A patched OpenDKIM of our own would make us its maintainer; signing in the panel's own milter, which already runs in the mail path, removes the program and most of what was built around it: proposed as roadmap row panel-dkim-signing.
A side finding of the reading: port 25 of the inbound relay accepts bare newlines - Postfix 3.7's default for the 2023 SMTP-smuggling parameter - one line fixes it: port25-bare-newline.
Everything below is the evidence and the prices. Versions are those of the
image (docs/development.md, the cited-package table); upstream source is
dev/upstream/; web sources are linked where a fact came from one.
1. What the image runs, and who maintains it now
| Component | In the image (bookworm) | In Debian trixie | Upstream |
|---|---|---|---|
| Debian | 12 bookworm, LTS since 2026-07-12, until 2028-06-30 (debian.org) | 13 trixie, stable since 2025-08-09 | — |
| Postfix | 3.7.11-0+deb12u1 (unchanged since the image was pinned; tracker) |
3.10.13-0+deb13u1 (trixie-security) |
3.11.7, stable branches 3.8–3.11 (3.11.7) |
| OpenDKIM | 2.11.0~beta2-8+deb12u1 |
2.11.0~beta2-9.1 (same code; a gcc-14 build fix, a path move, one verifier-side header patch) |
2.11.0-Beta2, 2018; no release since (releases, issue #422, unanswered) |
| libmilter | 8.17.1.9-2+deb12u2 |
8.18.1-6 |
— |
| Cyrus SASL | 2.1.28+dfsg-10 |
2.1.28+dfsg1-9 (same upstream) |
— |
| supervisor | 4.2.5-1 |
4.2.5-3 (same upstream) |
— |
| logrotate | 3.21.0-1 |
3.22.0-1 |
— |
| Go build stage | golang:1.26-bookworm |
golang:1.26-trixie exists (Docker Hub, 2026-10-10) |
— |
Postfix in bookworm is 2024 code with the 2026 fixes missing. Upstream issued three security batches this year, all without CVE ids:
- June 2026: unbounded reads of SMTP commands, responses and BDAT data ("large SMTP inputs"); a TLSA-parsing crash (DANE only - not us).
- August 2026 (3.11.6):
sixteen items, among them a remote memory exhaustion by many small BDAT
commands (smtpd, since 3.4), a remote smtpd crash by DATA without MAIL
FROM after a BDAT error, a policy bypass after an
smtpd_end_of_data_restrictionsreject (we set none), and the Milter left in "accept" state by the same bug. - September 2026 (3.11.7):
two SMTP-smuggling cases behind
smtpd_proxy_filter(we use none), a crash understrict_smtputf8(not set), a multi-line response over-read reachable by a malicious Milter, apostqueueuse-after-free.
Legacy releases 3.7.22 and 3.7.23 exist upstream but, as the announcements
say, "do not include the patches for out-of-support Postfix versions" of
June. Debian's tracker lists one 2026 CVE for bookworm's Postfix
(CVE-2026-43964,
a one-byte over-read, no-dsa) and no bookworm upload at all in 2026; the
LTS team works from CVE ids, and these batches have none. The exposed
listeners are ours: 465 and 587 to the Internet, 25 when the inbound relay
is on.
OpenDKIM has three open CVEs and nobody to fix them. Debian bug
#1149875
(2026-10-03, severity important, no reply from maintainer or security team
as of 2026-10-15) covers
CVE-2026-100888
(dkim_canon_selecthdrs, out-of-bounds write),
CVE-2026-100889
(dkim_qp_decode, off-by-one) and
CVE-2026-101277
(dkim_process_set, the tag tokenizer); all three "vendor was contacted
early but did not respond in any way". Read in dev/upstream/opendkim-2.11.0~beta2
for our configuration (Mode s, attached to 465/587 and to the panel's
local submission only; never to port 25):
dkim_qp_decodeis called fromdkim_sig_domainok(),dkim_sig_getreportinfo()anddkim_sig_getidentity()(libopendkim/dkim.c:1718, 7712, 7732, 7801) - verifier-side functions, reached fromdkim_eoh_verify(). Not on our path.dkim_process_setruns on everyDKIM-Signature:header of a message in either mode:dkim_header()(dkim.c:6447, the call at:6625) has no mode gate. On our path that header comes only from an authenticated application; the exposure is a tenant's, not the Internet's.dkim_canon_selecthdrsruns when signing as well as when verifying (dkim_canon_runheaders(),dkim-canon.c:1194, 1280). The write past the array islhdrs[shcnt] = NULL(dkim-canon.c:1052) once every header of the message has been matched and a further name follows. When verifying, the names are the signature'sh=, the attacker's. When signing, OpenDKIM composes the list itself - the headers present that are inSignHeaders(by default RFC 6376 § 5.4's SHOULD list,dkim_should_signhdrs[],dkim.c:223) plusOversignHeaders, ourFrom- so the condition is a message whose every header is in that list. Whether Postfix's ownReceived:keeps it out of reach I did not run; it is one ASan run withmilterteston the stand to know, and the answer changes only how soon § 4's row is wanted, not whether.
A patch for each exists in the researchers' write-ups (for dkim_qp_decode:
weitongli.com).
Nobody upstream will take them; Debian may yet carry them - the bug is
twelve days old - but the project behind the package is gone, and the
next finding meets the same silence.
2. The limits met so far, and what would lift each
| Limit (where it is worked around) | Own patched build | Debian trixie | Signing in the panel |
|---|---|---|---|
OpenDKIM logs through syslog(3) only (internal/syslogrelay, a supervisord program, /dev/log in the entrypoint; audit L30) |
yes, a stderr patch | no | yes - the program goes, and the relay with it (nothing else in the container uses syslog(3)) |
The panel reads Postfix's state from mail.log, postconf -h -x, postqueue -p/-j |
no: these are Postfix's interfaces; a patch would invent a private one to maintain | partly: 3.11 adds postconf -j (JSON) - not in trixie's 3.10 |
no (unchanged) |
The sender check reads From: the way OpenDKIM does: dkim_mail_parse() ported (internal/milter/mailparse.go, 327 lines), the 256-byte MAXADDRESS refusal, two diff/probe scripts against the C |
no: the port exists because two programs parse the line; a patch does not merge them (OpenDKIM's SenderMacro could pick the key by the envelope instead - it changes nothing, since the check already pins From: to the login's senders) |
no | yes: one parser, ours, and the key chosen by the login's domain, not by From: - the class "signed with another tenant's key" is gone by construction; From: still has to be the application's (DMARC alignment), with net/mail |
| The image held to bookworm's versions (Postfix 3.7.11, OpenDKIM 2.11.0~beta2) | yes, at the price in § 3 | yes for Postfix (3.10.13, security-tracked to 2028 and beyond in trixie LTS); OpenDKIM's version is the same everywhere | OpenDKIM's version stops mattering |
A socket file outlives a crashed OpenDKIM (health/socket.go, assumptions #38) |
yes, with a patch | no | yes - no second milter process |
Keys group-readable, RequireSafeKeys no, the selfpost group between panel and OpenDKIM |
no | no | yes - the panel reads its own keys; the group stays for Postfix's sasldb and maps |
SIGUSR1 through supervisord to reload the tables, which is what needs CAP_KILL in the compose file (cross-uid kill) |
no | no | yes - no tables, no signal; CAP_KILL can probably go (to verify on the stand: supervisord then signals only root's wrapper and the panel's own uid) |
smtpd_forbid_bare_newline defaults to no on Postfix < 3.9 (src/smtpd/smtpd.c:807); our port 25 does not set it |
yes | yes - the default is normalize since 3.9 |
no (it is Postfix's) - see § 5 |
Nothing in the Postfix column needs a patch. The limits are OpenDKIM's.
3. The options, priced
Prices are estimates in stages of the kind the 2026-10-12 audit used (a stage: write, tests, two reviews, e2e, rollout, commit). Nothing here was measured on the stand; where a number matters it says so.
A. Patched builds of both, carried by us - not recommended
- Gives: the stderr patch for OpenDKIM, the three CVE fixes, any option compiled in or out, the newest Postfix on the day we rebuild.
- Costs, once: a build stage per program (
apt-get source, quilt, a patch series,dpkg-buildpackage), its build-dependencies, the e2echeckCitedPackageVersionsrewritten for our version strings, every citation of3.7.11(331 lines in 65 files) and2.11.0~beta2(55 in 14) re-pointed at our tree. Image build time is not measured; from the size of the sources it is minutes, not seconds, on every rebuild. - Costs, for ever: Postfix issued seven releases in 2026; each is ours to notice, rebase the series on, rebuild and roll out, with no Debian security team between the announcement and the image. For OpenDKIM there is no upstream to rebase on at all: the series is the program, and the next CVE is ours to find and fix. One maintainer, two MTAs.
- Why not: § 2 shows no Postfix limit a patch lifts, and every OpenDKIM limit lifted by option E without taking OpenDKIM on.
B. Unpatched upstream build where the distribution's version is the limit - not recommended
- Gives: Postfix 3.11.7 today instead of trixie's 3.10.13 - the September batch two to four weeks earlier (trixie-security carried 3.10.11 → 3.10.12 → 3.10.13 between July and August; 3.10.14 is upstream now). For OpenDKIM there is no newer upstream to build.
- Costs: A's build stage and watching duty for Postfix, minus the patch series; Debian's own patches (paths, SASL and LMDB build options) become ours to reproduce.
- Why not: the gap to trixie-security is weeks, the duty is permanent.
C. A patch sent upstream instead of carried - nothing to send
- Postfix: no code change is needed (§ 2); Wietse Venema's project is responsive, but we have no patch for it.
- OpenDKIM: the project does not answer CVE reporters, the Debian maintainer or its own issue tracker. A patch sent there is a patch carried - option A.
D. The base image to Debian trixie - recommended, next code row → base-trixie
- Gives: Postfix 3.10.13 with the June and August batches and the
September one when trixie-security ships 3.10.14; Debian's security team
until 2028, then trixie LTS;
smtpd_forbid_bare_newline=normalizeby default;smtpd_hide_client_session(3.10: aReceived:header without the application's address - a product question of its own, not decided here); OpenSSL 3.5; libmilter 8.18.1. - Costs: one stage, one MINOR release. The work is the re-reading: the
331 citation lines against the 3.10.13 tree (most will hold word for
word;
src/paths are stable), with attention to what 3.8–3.10 changed on our paths - reject logging now carriessasl_method,sasl_username,sasl_sender(3.10, 20241023:refusals.go,inbound.goregexes), the queue id orNOQUEUEon an abnormal connection end and cleanup'scanceledline (3.10: new line shapes for the log tailer),postfix(1)logging to stderr when it is not a terminal (3.8: the wrapper's output under supervisord), DNS results capped at 100 records and stray CR/LF replaced by default (3.9), the removed parameters (3.9:permit_naked_ip_address,reject_maps_rbl,check_relay_domains- we set none). Then:dev/upstreamrefetched from trixie, the cited-package table andcheckCitedPackageVersions, the e2e sink image, the build stage tag,docs/assumptions-2026-10-14.md's header, the guide's "what the image contains". OpenDKIM's citations do not change (same upstream code). - Risk: a log line we parse changing shape unnoticed - the e2e suite and the fixtures labelled "seen" are the net; a day on the test server before the cut.
E. DKIM signing in the panel's own milter; OpenDKIM removed - recommended, after D → panel-dkim-signing
- Gives: every OpenDKIM row of § 2; about 1,500 lines gone (the relay
and its tests, the parser port and its tests, the probe scripts, the
table renderer and its reload,
opendkim.conf), two programs fewer under supervisord, two packages fewer in the image, no CVE watch on a dead project, probablyCAP_KILLdropped. Keys, selectors and the published DNS records are unchanged: the panel already generates the keys (internal/domain/dkim.go, PKCS#1 PEM) - nothing for an operator to do. - How:
github.com/emersion/go-msgauth/dkim(v0.7.0, 2025-04, MIT; the author of thego-milterwe run; the e2e suite already verifies with it). Read for this analysis:SignOptionsgivesrelaxed/simpleper side,HeaderKeyswith over-signing as RFC 6376 § 5.4.2 wants it (a name listed once more than present is kept inh=and nothing is hashed for it,sign.go), last-instance-first header picking (header.go), RSA and Ed25519, and a streamingNewSigner(the body is hashed as it passes, not buffered). The milter: headers and body chunks in,InsertHeader(0, "DKIM-Signature", …)at end of message - the shape of go-msgauth's owncmd/dkim-milter, some 450 lines. The key: by the login's domain on 465/587 (Bindings.DomainOf, which the sender check has), by the server's name for the panel's local submission (non_smtpd_milterspointed at the same socket - strict, as OpenDKIM is today). The header set:dkim_should_signhdrs[],Fromover-signed once; canonicalizationrelaxed/simple, as now. - Costs: two to three stages, one MINOR release: the signer and its
tests; the removal (code, image, compose, supervisord, entrypoint,
Health's liveness set, Resync's steps, docs: architecture § OpenDKIM,
security, guide, development's table, assumptions § 3); e2e with an
independent verifier as the oracle - go-msgauth must not check its
own work:
opendkim-testmsgin the sink image or rspamd's verifier, the waymailparse-diff.shholds the port to the C today - and a message to a real mailbox before the cut. - Risk: a signing defect fails DMARC for every tenant at once; the oracle and the canary are the answer. go-msgauth is a small library with a quiet release pace - vendorable if it ever matters.
- Why after D: D's exposure is remote and unauthenticated (465/587), E's is a tenant's; D is one stage, E is three; neither waits on the other (E re-reads no Postfix citation, D no OpenDKIM one).
4. Recommendation
In this order, each a roadmap row of its own, none of them an own build:
- port25-bare-newline - one line in
build/postfix-config.sh, a PATCH release, now (§ 5). - base-trixie - option D, one stage, MINOR.
- panel-dkim-signing - option E, two to three stages, MINOR.
Options A, B and C are declined with the arguments above; what would bring A back is a limit of Postfix's that its interfaces cannot answer, and none is known.
5. Side finding: bare newlines on port 25
Postfix 3.7.11 has the 2023 SMTP-smuggling parameter
(smtpd_forbid_bare_newline, src/global/mail_params.h:4249) with the
default no "for Postfix < 3.9" (src/smtpd/smtpd.c:807); its
RELEASE_NOTES recommend normalize, which 3.9 made the default. The
inbound relay's listener (build/postfix-config.sh, the smtp/inet
block) does not set it, so with INBOUND_RELAY_ENABLE=true the relay is
the receiving side the smuggling research described: a bare <LF>.<LF>
inside DATA ends the message early and what follows is read as the next
transaction. Ports 465 and 587 are not the case that matters - the
smuggled transaction is still the authenticated session's, under its
sender bindings and milters. The fix is
smtp/inet/smtpd_forbid_bare_newline=normalize with an e2e check on port
25 (a bare-LF DATA, one message delivered); a PATCH release. Trixie's 3.10
would set it by default, but the relay should not wait for that row.
A smaller one: opendkim-tools is installed in the image and used by
nothing at run time (the key is generated in Go; the probe script runs on
the server from source). It goes with option E, or with D if E waits.