Files
selfpost/build/Dockerfile
T
mixandClaude Opus 5.5 faa3c11ae7
test / test (push) Waiting to run
panel-dkim-signing stage 2: OpenDKIM removed
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>
2026-10-16 01:05:39 +03:00

186 lines
9.7 KiB
Docker

# syntax=docker/dockerfile:1
#
# SelfPost — single Debian-slim image running postfix + panel under
# supervisord. Build from the repository root:
#
# docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
# ---- build stage -------------------------------------------------------------
FROM golang:1.26-trixie AS build
WORKDIR /src
# Version stamped into both binaries; MUST match the image tag.
ARG VERSION=dev
# Module metadata first for layer caching.
COPY go.mod go.sum ./
RUN go mod download
COPY cmd ./cmd
COPY internal ./internal
ENV CGO_ENABLED=0
RUN go vet ./... \
&& go build -trimpath \
-ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=${VERSION}" \
-o /out/panel ./cmd/panel \
&& go build -trimpath \
-ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=${VERSION}" \
-o /out/selfpost-backup ./cmd/selfpost-backup
# ---- runtime stage -----------------------------------------------------------
FROM debian:trixie-slim AS runtime
ENV DEBIAN_FRONTEND=noninteractive
# Preseed Postfix so its install is non-interactive and yields a working
# main.cf. The real relay configuration is generated by the panel.
# (postfix 3.10.13-0+deb13u1, debian/templates:14 and :50:
# postfix/main_mailer_type and postfix/mailname are the questions the install
# asks; debian/postfix.postinst:156-171 writes main.cf for any answer but "No
# configuration".)
# openssl is named because postfix-cert-reload.sh runs it, not left to arrive
# with ca-certificates. python3 arrives with supervisor (supervisor 4.2.5-3,
# debian/control:29, ${python3:Depends}) - crashexit.py. sasl2-bin brings
# saslpasswd2 (cyrus-sasl2 2.1.28+dfsg1-9, debian/rules:207,
# debian/sasl2-bin.install:3, usr/sbin) and depends on db-util
# (debian/control:36); libsasl2-modules brings the PLAIN and LOGIN plugins
# (debian/libsasl2-modules.install:4 and :6); the sasldb auxprop plugin is in
# libsasl2-modules-db (debian/libsasl2-modules-db.install:1), a Depends of
# libsasl2-2 (debian/control:65). db-util is named for /usr/bin/db_dump, which
# the panel runs (internal/app/sasl.go); that db-util ships that path on
# trixie is not in a source read here - the image build and the e2e sasldb
# test say whether it does.
# No DKIM package is installed: the panel signs itself (internal/milter,
# sign.go) and makes the keys in Go (internal/domain/dkim.go); OpenDKIM left
# the image in 2.8.0 (docs/plans/panel-dkim-signing.md).
# systemd-standalone-sysusers is named so that cron-daemon-common, which
# logrotate brings (logrotate 3.22.0-1, debian/control: "Depends: cron |
# anacron | cron-daemon | systemd-sysv"), creates its group with it rather
# than with the whole of systemd, the first alternative of its "Depends:
# systemd | systemd-standalone-sysusers | systemd-sysusers" (dpkg -s in the
# image, 2026-10-15). Nothing here runs systemd.
RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
&& echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
postfix \
sasl2-bin \
libsasl2-modules \
db-util \
supervisor \
logrotate \
ca-certificates \
openssl \
curl \
systemd-standalone-sysusers \
&& rm -rf /var/lib/apt/lists/*
# Debian's postfix runs its chroot sync on every "postfix check" and "postfix
# start-fg" (postfix 3.10.13-0+deb13u1, debian/patches/
# debian-run-configure-instance-from-create-missing.patch); with no service
# chrooted (build/postfix-config.sh) it would only copy /etc/resolv.conf and
# the like into the queue under /data on each start. SYNC_CHROOT empty turns
# it off (debian/configure-instance.in:22-24 and :44; debian/README.Debian:136-138).
RUN printf 'SYNC_CHROOT=\n' > /etc/default/postfix
# Unprivileged user for the panel process, with ids of its own outside the
# system range for the same reason as selfpost's below: its files under
# /data show on the host by number, and a number from that range is a
# system group of the host's too (useradd gave the panel group 998 in the
# trixie image, systemd-network on the test server's host; bookworm's
# 999:999 was the host's systemd-journal group). The entrypoint gives /data
# back to it by name on every start, so files of the old ids follow.
RUN groupadd --system --gid 10999 panel && useradd --system --uid 10999 --gid panel --no-create-home --shell /usr/sbin/nologin panel
# Shared group bridging the unprivileged services: the panel writes the
# application SASL accounts (sasldb2) and the Postfix sender map and opens the
# milter sockets, while Postfix (another user) must read and reach them.
# Membership in this group - plus setgid dirs under /data and /run/selfpost
# (set up in entrypoint.sh) - lets Postfix read the sasldb2/sender map and
# connect to the panel's milters. The DKIM keys are the panel's alone. (The
# supervisor control socket has a group of its own, `supervisor`, below.)
# The id is fixed, because the host sees it: an operator gives the TLS
# private key on the host to this group by number, so that the panel can put
# it into a full backup (guide § Taking a full backup). It lies outside the
# range the distribution hands out to system groups, 100-999 (adduser 3.152,
# /etc/adduser.conf: "Default: FIRST_SYSTEM_GID=100, LAST_SYSTEM_GID=999";
# passwd 1:4.17.4-2, /etc/login.defs: SYS_GID_MIN 101, SYS_GID_MAX 999 - both
# read in the image, 2026-10-15), so no package can take it, and far above
# the 1000 a host starts its users' groups at, so a host is unlikely to have
# a group of that number that would read the key too. Until 2.7.0 it was
# 998, inside that range: in the trixie image systemd took 998 for
# systemd-network and the build failed, and on the test server's host 998
# is systemd-network too (seen 2026-10-15).
# groupadd(8) refuses a --gid already in use, so an id taken after all would
# fail this build rather than share the group.
RUN groupadd --system --gid 10998 selfpost \
&& usermod -aG selfpost panel \
&& usermod -aG selfpost postfix
# The panel reads the mail queue with `postqueue -p` (and the status page's
# queue card). postqueue is setgid postdrop, which normally gives it the group
# needed to reach Postfix's showq socket - but the documented deployment runs
# with `no-new-privileges`, which disables setgid transitions, so the panel
# would always see "Permission denied". Making `panel` a real member of postdrop
# grants the same access without relying on a setgid escalation the hardening
# deliberately forbids. The same membership is what lets the panel submit its
# own notifications with sendmail(1) - postdrop, too, is setgid and would fail
# to write the maildrop directory (internal/notify/mailer.go). It does not let
# the panel bypass any Postfix restriction that a local user does not already
# have through the world-executable sendmail.
# (Postfix 3.10.13, conf/postfix-files:140-141: postdrop and postqueue are
# root:$setgid_group 2755; Debian sets them root:postdrop 02555, postfix
# 3.10.13-0+deb13u1, debian/postfix.postinst:134-141. Linux,
# Documentation/userspace-api/no_new_privs.rst:
# with no_new_privs, execve() no longer changes the gid for a setgid file.)
RUN usermod -aG postdrop panel
# The supervisord control socket's group (supervisord.conf, [unix_http_server]):
# the panel alone - not Postfix's user, which shares `selfpost` for the files
# under /data.
RUN groupadd --system supervisor \
&& usermod -aG supervisor panel
# Runtime directories: milter sockets and the consolidated persistent root.
RUN mkdir -p /run/selfpost /data \
&& chown panel:panel /run/selfpost /data
COPY --from=build /out/panel /usr/local/bin/panel
COPY --from=build /out/selfpost-backup /usr/local/bin/selfpost-backup
# Licence text shipped with the image (AGPL-3.0 conveyance). The panel also
# serves the same text at /license from an embedded copy. OFL.txt is the SIL
# Open Font License 1.1 that must travel with the IBM Plex WOFF2 files
# embedded in the panel binary (NOTICE).
COPY --chmod=0644 LICENSE NOTICE /usr/share/doc/selfpost/
COPY --chmod=0644 internal/web/view/static/OFL.txt /usr/share/doc/selfpost/OFL.txt
# File modes are pinned here so a build context copied from a checkout without
# POSIX permissions (e.g. Windows tar sync) cannot land group-writable config
# that logrotate would silently ignore — see docs/development.md § Building
# binaries and the image.
COPY --chmod=0644 build/logrotate-mail.conf /etc/logrotate.d/mail
COPY --chmod=0755 build/postfix-wrapper.sh /usr/local/bin/postfix-wrapper.sh
COPY --chmod=0755 build/postfix-reload.sh /usr/local/bin/postfix-reload.sh
COPY --chmod=0755 build/postfix-config.sh /usr/local/bin/postfix-config.sh
COPY --chmod=0755 build/postfix-cert-reload.sh /usr/local/bin/postfix-cert-reload.sh
COPY --chmod=0755 build/logrotate-loop.sh /usr/local/bin/logrotate-loop.sh
COPY --chmod=0755 build/crashexit.py /usr/local/bin/crashexit.py
COPY --chmod=0755 build/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chmod=0644 build/supervisord.conf /etc/supervisor/supervisord.conf
# Published submission ports: 465 (smtps, primary) and 587 (submission, optional),
# inbound SMTP on 25 when INBOUND_RELAY_ENABLE=true, plus the panel on 8080.
# Outbound delivery dials remote MXs on 25 as a client even when inbound is off.
EXPOSE 8080 465 587 25
# Liveness probe: panel HTTP plus mail-path processes (panel, postfix).
# Does not verify TLS, DNS, or end-to-end delivery — see docs/guide.md Operations.
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1
# The entrypoint fixes /data ownership (bind mount) as root, then execs
# supervisord, which becomes PID 1 and owns process supervision.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]