test / test (push) Waiting to run
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>
160 lines
6.8 KiB
Plaintext
160 lines
6.8 KiB
Plaintext
; SelfPost process supervision.
|
|
;
|
|
; Start ORDER is enforced by priority=: the panel (which opens the sender
|
|
; check's, the DKIM signer's and the journal-milter's sockets), then the
|
|
; Postfix wrapper — which additionally blocks until the milter sockets are ready
|
|
; before starting Postfix. (supervisor 4.2.5, docs/configuration.rst,
|
|
; [program:x] "priority": "the start and shutdown ordering"; supervisor/
|
|
; supervisord.py, runforever(), transitions every group in that order in one
|
|
; pass, with no wait for a program to be ready.)
|
|
;
|
|
; If a managed process of the mail path exhausts its restart retries (FATAL),
|
|
; or exits unexpectedly five times in ten minutes, the crashexit event listener
|
|
; brings the whole container down so Docker's restart policy starts it again
|
|
; (the same container: docs.docker.com, "Start containers automatically",
|
|
; restart policies), rather than lingering with a dead component. A program
|
|
; reaches FATAL only through BACKOFF, by exits that come before its startsecs
|
|
; (supervisor 4.2.5, supervisor/process.py, Subprocess.finish() and
|
|
; transition()): an exit after it is EXITED and simply restarted, for ever -
|
|
; which the count of exits covers (audit 2026-10-12, L32). logrotate's,
|
|
; postfix-reload's and cert-reload's failures are left on Server > Health
|
|
; (build/crashexit.py).
|
|
|
|
[supervisord]
|
|
nodaemon=true
|
|
user=root
|
|
logfile=/dev/null
|
|
logfile_maxbytes=0
|
|
pidfile=/run/supervisord.pid
|
|
loglevel=info
|
|
|
|
; The control socket is reachable by the `supervisor` group, whose one member
|
|
; is the panel (the unprivileged `panel` user): it asks supervisord to run the
|
|
; Postfix reload, and for the processes' state. Whoever reaches the socket can
|
|
; stop, start or restart any program, so it is not the shared `selfpost`
|
|
; group's: Postfix is in that one for the files under /data, and a fault in
|
|
; it must not reach every other program (audit 2026-10-12, stage 5). Not
|
|
; reachable by world. No HTTP auth is configured because access is gated by
|
|
; socket permissions, not the network. (supervisor 4.2.5,
|
|
; docs/configuration.rst, [unix_http_server]: chmod and chown set the socket's
|
|
; mode and owner; without username and password no authentication is asked.)
|
|
[unix_http_server]
|
|
file=/run/supervisor.sock
|
|
chown=root:supervisor
|
|
chmod=0770
|
|
|
|
[rpcinterface:supervisor]
|
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
|
|
|
[supervisorctl]
|
|
serverurl=unix:///run/supervisor.sock
|
|
|
|
; user=panel also gives the panel its supplementary groups, selfpost and
|
|
; postdrop (build/Dockerfile): supervisor 4.2.5, supervisor/options.py,
|
|
; drop_privileges(), os.setgroups() from the user's memberships in
|
|
; grp.getgrall().
|
|
[program:panel]
|
|
command=/usr/local/bin/panel
|
|
user=panel
|
|
priority=200
|
|
autostart=true
|
|
autorestart=true
|
|
startretries=3
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
; startsecs is MILTER_WAIT_TIMEOUT plus ten seconds (build/entrypoint.sh): the
|
|
; wrapper's exit when the milter sockets never come counts as too quick, and
|
|
; so does a Postfix that will not start. The price: for that long after each
|
|
; start the program is STARTING, not RUNNING (supervisor 4.2.5,
|
|
; supervisor/options.py, processes_from_section(): startsecs is expanded like
|
|
; any value, %(ENV_x)s from supervisord's environment; assumptions C-V6).
|
|
; stopwaitsecs covers "postfix stop", which waits five seconds for master
|
|
; before it kills (build/postfix-wrapper.sh).
|
|
[program:postfix]
|
|
command=/usr/local/bin/postfix-wrapper.sh
|
|
priority=300
|
|
autostart=true
|
|
autorestart=true
|
|
startsecs=%(ENV_POSTFIX_STARTSECS)s
|
|
startretries=3
|
|
stopwaitsecs=15
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
; One-shot reload helper. The panel runs unprivileged and cannot run `postfix
|
|
; reload` itself, and it cannot signal the Postfix master directly: `postfix
|
|
; start-fg` forks a separate master process, so a signal to the supervised
|
|
; foreground process never reaches it. Instead the panel asks
|
|
; supervisord — over the group-accessible control socket — to run this program,
|
|
; which runs the canonical `postfix reload` as root - or exits 75 when no
|
|
; master runs (build/postfix-reload.sh). autostart is off (it only runs on
|
|
; demand). A program that cannot even be spawned twice in a row ends FATAL
|
|
; (startretries=1); crashexit leaves that to the page that asked for the
|
|
; reload, which reports it (build/crashexit.py; review of stage 2c, F6). (start-fg: see build/postfix-wrapper.sh. supervisor
|
|
; 4.2.5, supervisor/process.py, Subprocess.finish(): with startsecs=0 no exit
|
|
; counts as "too quickly", so even a failed run ends EXITED - never BACKOFF,
|
|
; never FATAL - with its exit status kept, which the panel reads
|
|
; (internal/supervisor, RunOnce). Its resting state is STOPPED until the first
|
|
; run, EXITED after it; assumptions C-V10.)
|
|
[program:postfix-reload]
|
|
command=/usr/local/bin/postfix-reload.sh
|
|
autostart=false
|
|
autorestart=false
|
|
startsecs=0
|
|
startretries=1
|
|
exitcodes=0
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
; TLS-certificate refresh. Once a day it compares the certificate the
|
|
; reverse-proxy renews in the read-only mount with the copy Postfix reads, and
|
|
; when they differ copies it again and runs `postfix reload`.
|
|
; Long-running (it loops), runs as root so it can reload Postfix, and never exits
|
|
; non-zero, so it neither trips the crashexit listener nor needs restarting.
|
|
[program:cert-reload]
|
|
command=/usr/local/bin/postfix-cert-reload.sh
|
|
priority=400
|
|
autostart=true
|
|
autorestart=true
|
|
startsecs=0
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
; Periodic logrotate for /data/log/mail.log (daily, 14 files kept). Runs as
|
|
; root so logrotate can read/rotate the log. Exits non-zero at once when the
|
|
; config is group/other-writable (logrotate would ignore it silently): with
|
|
; startsecs=1 that exit is too quick; once startretries are spent the program
|
|
; is FATAL, and Server > Health shows it. With startsecs=0 it was restarted
|
|
; every second for ever, RUNNING and EXITED in turn (supervisor 4.2.5,
|
|
; supervisor/process.py, Subprocess.finish(); assumptions C-V5). crashexit
|
|
; leaves this FATAL alone: logrotate is not on the mail path.
|
|
[program:logrotate]
|
|
command=/usr/local/bin/logrotate-loop.sh
|
|
priority=400
|
|
autostart=true
|
|
autorestart=true
|
|
startsecs=1
|
|
startretries=3
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
; supervisor 4.2.5, supervisor/process.py: a program enters FATAL only from
|
|
; BACKOFF, once startretries are spent; a stop asked of supervisord ends in
|
|
; STOPPED (Subprocess.finish(), self.killing), which this listener never sees.
|
|
[eventlistener:crashexit]
|
|
command=/usr/local/bin/crashexit.py
|
|
events=PROCESS_STATE_FATAL,PROCESS_STATE_EXITED
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|