Phase 5: full outbound Postfix relay (465/587 SASL+TLS, sender binding, milters)

Generate the relay config from the environment at container start
(build/postfix-config.sh, run from entrypoint.sh):

- smtps 465 (implicit TLS) primary + optional submission 587 (STARTTLS),
  chroot=n so smtpd reaches the sasldb2/sender map under /data.
- Cyrus SASL against the panel-maintained sasldb2; realm left implicit so the
  authenticated name equals the bare login in smtpd_sender_login_maps.
- reject_sender_login_mismatch + relay/recipient restrictions with no
  permit_mynetworks: credentials-only, open relay impossible (spec 5, 5.1).
- TLS cert/key from TLS_CERT_FILE/TLS_KEY_FILE; daily postfix reload picks up
  renewed certs (postfix-cert-reload.sh under supervisord, spec 5.2).
- anvil level-1 rate limit from env (spec 5 p.5).
- Milter chain with per-milter action: OpenDKIM strict (tempfail), journal
  fail-open (accept) so monitoring never blocks the relay (spec 7.3).

Two integration fixes found on the server:
- postconf -F '*/*/chroot=n': Debian's chrooted delivery agent can't read
  /etc/resolv.conf, so MX lookups failed and mail never left.
- entrypoint sets /run/opendkim and /run/selfpost to group selfpost + setgid,
  and the journal stub chmods its socket 0660, so postfix can connect to both
  milter sockets (strict OpenDKIM was milter-rejecting all mail otherwise).

Verified on selfpost.example.com: gofmt/vet/test green, image builds; container
e2e — 465 auth+send DKIM-signed (d=domain,s=selfpost), 587 STARTTLS auth,
cross-domain sender 553, list-mode per-address binding, unauth relay 554,
real outbound delivery reaching the recipient MX over TLS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:37:16 +03:00
parent c6eeb30258
commit 2c7f0da3d8
6 changed files with 247 additions and 7 deletions
+9 -7
View File
@@ -51,10 +51,6 @@ RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Postfix logs to a plain file (via its built-in postlogd) so the panel's
# log-tailer has something to follow and container logging works without syslog.
RUN postconf -e "maillog_file=/var/log/mail.log"
# Unprivileged user for the panel process (spec 7.6.8).
RUN useradd --system --no-create-home --shell /usr/sbin/nologin panel
@@ -80,13 +76,19 @@ COPY --from=build /out/selfpost-backup /usr/local/bin/selfpost-backup
COPY build/opendkim.conf /etc/opendkim.conf
COPY build/postfix-wrapper.sh /usr/local/bin/postfix-wrapper.sh
COPY build/postfix-config.sh /usr/local/bin/postfix-config.sh
COPY build/postfix-cert-reload.sh /usr/local/bin/postfix-cert-reload.sh
COPY build/crashexit.py /usr/local/bin/crashexit.py
COPY build/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY build/supervisord.conf /etc/supervisor/supervisord.conf
RUN chmod +x /usr/local/bin/postfix-wrapper.sh /usr/local/bin/crashexit.py /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/postfix-wrapper.sh /usr/local/bin/postfix-config.sh \
/usr/local/bin/postfix-cert-reload.sh /usr/local/bin/crashexit.py \
/usr/local/bin/entrypoint.sh
# 8080 panel; 25 outbound; 465/587 inbound submission (used from Phase 5).
EXPOSE 8080 25 465 587
# Published submission ports: 465 (smtps, primary) and 587 (submission, optional)
# plus the panel on 8080. Outbound delivery dials remote MXs on 25 as a client,
# which needs no inbound listener or EXPOSE.
EXPOSE 8080 465 587
# The entrypoint fixes /data ownership (bind mount) as root, then execs
# supervisord, which becomes PID 1 and owns process supervision (spec 4).