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>
151 lines
8.6 KiB
YAML
151 lines
8.6 KiB
YAML
|
|
# SelfPost — default deployment, reverse-proxy = Apache.
|
|
#
|
|
# This file only runs SelfPost itself. Apache is assumed to already be installed
|
|
# on the HOST (the target audience for this project typically runs Apache there
|
|
# already) and reverse-proxies HTTPS for the panel; it is not containerised
|
|
# here. See ../apache/selfpost-vhost.conf for a ready vhost fragment, and
|
|
# docs/guide.md "Reverse proxy (mandatory)" for the certbot steps that produce the PEM files
|
|
# this compose file mounts.
|
|
#
|
|
# Usage:
|
|
# 1. Copy this file (and .env.example as .env) next to your own ./data and
|
|
# ./certs directories, or adjust the paths below.
|
|
# 2. Fill in .env (at minimum SELFPOST_HOSTNAME). Put PEM files in ./certs —
|
|
# TLS paths are fixed in this file to match that bind mount, not .env.
|
|
# 3. docker compose up -d
|
|
#
|
|
# The image tag below is FIXED on purpose: backup restore compares the manifest
|
|
# version against the running binary's version, so ":latest" would make that
|
|
# check meaningless. Bump the tag deliberately when you want to upgrade.
|
|
# The 2.x line is not on ghcr.io yet: build it locally under this exact name
|
|
# (README, Quick start) and Compose will use it without pulling.
|
|
|
|
services:
|
|
selfpost:
|
|
image: ghcr.io/mixeme/selfpost:2.7.0
|
|
restart: unless-stopped
|
|
environment:
|
|
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
|
# Path Postfix reads inside the container — matches the certs bind mount
|
|
# below. Point these at your reverse-proxy's PEM output.
|
|
TLS_CERT_FILE: /etc/postfix/tls/fullchain.pem
|
|
TLS_KEY_FILE: /etc/postfix/tls/privkey.pem
|
|
# Set to true to also publish RFC 6409 submission (587/STARTTLS) alongside
|
|
# the primary 465/smtps listener.
|
|
SUBMISSION_ENABLE: "${SUBMISSION_ENABLE:-false}"
|
|
INBOUND_RELAY_ENABLE: "${INBOUND_RELAY_ENABLE:-false}"
|
|
DMARC_REPORTS_ENABLE: "${DMARC_REPORTS_ENABLE:-false}"
|
|
DMARC_RATE_LIMIT_MESSAGES_PER_IP: "${DMARC_RATE_LIMIT_MESSAGES_PER_IP:-20}"
|
|
DMARC_MESSAGE_SIZE_LIMIT: "${DMARC_MESSAGE_SIZE_LIMIT:-5242880}"
|
|
INBOUND_ANTISPAM_MILTER: "${INBOUND_ANTISPAM_MILTER:-}"
|
|
INBOUND_ANTISPAM_MILTER_ACTION: "${INBOUND_ANTISPAM_MILTER_ACTION:-accept}"
|
|
INBOUND_RATE_LIMIT_MESSAGES_PER_IP: "${INBOUND_RATE_LIMIT_MESSAGES_PER_IP:-20}"
|
|
INBOUND_MESSAGE_SIZE_LIMIT: "${INBOUND_MESSAGE_SIZE_LIMIT:-26214400}"
|
|
# Level-1 backstop rate limit (anvil); per-domain/app limits (level 2) are
|
|
# configured later from the panel itself.
|
|
RATE_LIMIT_MESSAGES_PER_IP: "${RATE_LIMIT_MESSAGES_PER_IP:-100}"
|
|
RATE_LIMIT_WINDOW_SECONDS: "${RATE_LIMIT_WINDOW_SECONDS:-3600}"
|
|
# How long the send log keeps rows before the background sweep deletes
|
|
# them — the main driver of /data growth over time.
|
|
SEND_LOG_RETENTION_DAYS: "${SEND_LOG_RETENTION_DAYS:-90}"
|
|
INBOUND_SPAM_LOG_RETENTION_DAYS: "${INBOUND_SPAM_LOG_RETENTION_DAYS:-90}"
|
|
# Sliding idle timeout for the panel login session, in days.
|
|
PANEL_SESSION_IDLE_DAYS: "${PANEL_SESSION_IDLE_DAYS:-7}"
|
|
# Resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly, so they
|
|
# report what the internet sees rather than what this host's own stub
|
|
# resolver synthesises. Empty uses public defaults.
|
|
SELFPOST_DNS_RESOLVERS: "${SELFPOST_DNS_RESOLVERS:-}"
|
|
# Reverse-proxy CIDRs allowed to supply X-Forwarded-For for login/setup
|
|
# rate-limiting. See docs/guide.md "Environment variables" — wrong value lets a
|
|
# client spoof its rate-limit key.
|
|
TRUSTED_PROXY_CIDR: "${TRUSTED_PROXY_CIDR:-}"
|
|
volumes:
|
|
# All persistent state lives under /data: SQLite DB, DKIM keys, sasldb2,
|
|
# sender map, setup token. Back this up (panel button or the
|
|
# selfpost-backup CLI) before you touch it directly. Postfix's mail.log is
|
|
# here too (./data/log, rotated daily, 14 files kept) so the delivery
|
|
# lines that resolve a queued send-log row survive a container recreate;
|
|
# it is diagnostics and is left out of backups.
|
|
- ./data:/data
|
|
# Read-only project root for full backups: docker-compose.yml, .env, and
|
|
# certs/ are archived alongside /data (architecture.md § Persistence).
|
|
- .:/selfpost-deploy:ro
|
|
# Read-only: SelfPost only ever reads certificates, never manages them.
|
|
# ./certs holds fullchain.pem and privkey.pem: the files themselves, or
|
|
# links to where your reverse proxy's certbot keeps them.
|
|
- ./certs:/etc/postfix/tls:ro
|
|
# With certbot on the host (docs/guide.md, "Reverse proxy (mandatory)", Apache):
|
|
# certs/ holds links into /etc/letsencrypt/live/, and certbot's live/
|
|
# files are themselves relative links into archive/ - so the whole
|
|
# directory is mounted, at the same path, for the links to lead
|
|
# somewhere inside the container. Uncomment it then.
|
|
# - /etc/letsencrypt:/etc/letsencrypt:ro
|
|
ports:
|
|
# 465 (smtps, primary) and optionally 587 (submission/STARTTLS) are
|
|
# published directly — mail traffic bypasses Apache entirely, it only ever
|
|
# proxies the panel's HTTP(S). The panel itself (8080) is intentionally
|
|
# NOT published here: Apache reaches it over the host network at
|
|
# 127.0.0.1:8080 (see the vhost fragment), so the panel is never directly
|
|
# reachable from the internet without TLS.
|
|
- "465:465"
|
|
# 587 is mapped even when SUBMISSION_ENABLE=false; Postfix listens only
|
|
# when the variable is true — see docs/guide.md "Ports".
|
|
- "587:587"
|
|
# 25 is mapped even when INBOUND_RELAY_ENABLE=false; Postfix listens only
|
|
# when the variable is true.
|
|
- "25:25"
|
|
- "127.0.0.1:8080:8080"
|
|
# Hardening. SelfPost's entrypoint still needs to run as
|
|
# root very briefly to fix /data ownership and normalise permissions
|
|
# under the shared `selfpost` group (see build/entrypoint.sh) before
|
|
# supervisord drops the panel to an unprivileged user — so this cannot be
|
|
# `user: panel` or a fully read-only rootfs without breaking that startup
|
|
# self-healing. What IS applied: no privilege escalation past what the
|
|
# image already grants, and every Linux capability dropped except the
|
|
# small set the root startup phase and Postfix genuinely need:
|
|
# - NET_BIND_SERVICE — bind 465/587 and inbound 25 below 1024;
|
|
# - CHOWN — entrypoint re-owns /data (bind mount) to `panel`;
|
|
# - FOWNER — entrypoint then chmods those now panel-owned /data
|
|
# dirs/files while still root (owner-check bypass);
|
|
# - FSETID — set the setgid bit (2750) on the shared /data dirs
|
|
# when the process gid differs from the dir's group;
|
|
# - SETUID/SETGID — supervisord drops the panel to the unprivileged
|
|
# `panel` user; Postfix switches to its own users;
|
|
# - DAC_OVERRIDE — cross-user file access within the `selfpost` group.
|
|
# - KILL — root signals processes of other uids: supervisord
|
|
# stops and restarts the panel (supervisor 4.2.5,
|
|
# supervisor/process.py:467, options.kill()), and
|
|
# Postfix's master ends its children, which run as
|
|
# `postfix` (Postfix 3.10.13, src/master/master_sig.c:196,
|
|
# master_sigdeath(); master_spawn.c:365,
|
|
# master_delete_children()). Without it kill() fails
|
|
# with EPERM although the caller is root (kill(2),
|
|
# capabilities(7); measured on the test server
|
|
# 2026-10-16: root with SETUID/SETGID and no KILL
|
|
# cannot kill a process of uid 65534). Until 2.8.0
|
|
# it also carried OpenDKIM's USR1 reload.
|
|
# FOWNER/FSETID are required by build/entrypoint.sh's permission
|
|
# self-healing; without them chmod fails with EPERM and the container
|
|
# crash-loops on start.
|
|
# Each capability above as capabilities(7) defines it (man7.org,
|
|
# "Capabilities list"); SETUID/SETGID are also what pipe(8) uses to become
|
|
# user=panel for dmarc-ingest. Not listed, so dropped: SYS_CHROOT, which
|
|
# chroot(2) needs - build/postfix-config.sh turns Postfix's chroot off.
|
|
# no-new-privileges only changes execve() (Linux,
|
|
# Documentation/userspace-api/no_new_privs.rst): root may still call
|
|
# setuid()/setgid() with these capabilities.
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
cap_add:
|
|
- NET_BIND_SERVICE
|
|
- CHOWN
|
|
- FOWNER
|
|
- FSETID
|
|
- SETUID
|
|
- SETGID
|
|
- DAC_OVERRIDE
|
|
- KILL
|