Files
selfpost/build/entrypoint.sh
T
mixandClaude Opus 5.5 eea40bcd49 inbound-antispam-panel: filter lists, written as rspamd maps
Step 5/12 of docs/plans/inbound-antispam-panel.md.

Store: AddSpamListEntry, ListSpamListEntries, DeleteSpamListEntry.
internal/inbound/lists.go validates and normalises an entry - sender
address, sender domain, client address or network (canonical text, an
IPv4-mapped address as IPv4, /0 refused) - and writes the lists as six
map files under /data/antispam (ANTISPAM_DIR), each atomically, 0644 in
a 0755 directory: the filter is another user in another container. All
six are written on every change and on every start with the relay on,
empty ones included. An entry whose maps could not be written is taken
back out of the table. The writer refuses a stored value with
whitespace, a newline or # in it.

The plan's allow.map / deny.map became one file per list and entry
type: to rspamd an IP map and a sender map are different map types.

deploy/antispam/rspamd/multimap.conf holds the rules; the fragment
mounts it and ./data/antispam into the sidecar. Deny is a prefilter
reject, allow a symbol weighing -100. Tried in a throwaway rspamd 3.11
container on the test server:
  - with allow as a prefilter accept, an address on both lists was
    accepted and an IP on both was refused - the rule that ran first
    decided; with the weight, deny won in all five mixed cases;
  - IPv4, IPv6, address (any case), domain (not its subdomains) match;
  - a rewritten file is reread in about ten seconds, no restart;
  - files that appear after rspamd started are loaded the same way.

No page edits the lists yet (step 6).

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok - with the relay on
/data/antispam is 755 and holds the six maps, 644, empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:10 +03:00

182 lines
8.9 KiB
Bash

#!/bin/sh
# Container entrypoint (runs as root, PID 1 until it execs supervisord).
#
# SELFPOST_HOSTNAME is checked first (plan B.3): it must fail fast with a clear
# message before /data normalisation or Postfix config, so a bad identity never
# looks like a permissions or packaging problem (and so the e2e hostname-gate
# tests see the FATAL text rather than an earlier set -e abort).
set -e
# SELFPOST_HOSTNAME is an identity, not a setting with a safe default: it must
# simultaneously match the PTR/rDNS record, the certificate CN/SAN, and the
# Cyrus SASL realm. The panel (main.go saslRealm()) and postfix-config.sh each
# fall back independently when it's unset — to `localhost` and to the container
# hostname respectively — so accounts get written under one realm and looked up
# under another and authentication silently fails for every application, while
# HELO also stops matching the PTR record and mail that does go out lands in
# spam. No fallback can be correct, so fail loudly here, before either side of
# that split has a chance to run, rather than leave a green panel with broken
# mail.
if [ -z "$SELFPOST_HOSTNAME" ]; then
cat >&2 <<'EOF'
FATAL: SELFPOST_HOSTNAME is not set.
This is the mail server's identity: it becomes the Postfix HELO/EHLO name,
the Cyrus SASL realm that application passwords are looked up under, and it
must match the TLS certificate's CN/SAN as well as this server's PTR (reverse
DNS) record. There is no safe default — guessing any one of these wrong
breaks authentication for every application or sends outgoing mail to spam,
silently.
Set it to the mail server's fully-qualified domain name, e.g.:
SELFPOST_HOSTNAME=mail.example.com
in the .env file next to your docker-compose.yml (see deploy/.env.example).
EOF
exit 1
fi
case "$SELFPOST_HOSTNAME" in
*[\ \ ]* | *://* | *:* )
echo "FATAL: SELFPOST_HOSTNAME must be a bare hostname (no scheme, port, or spaces): \"$SELFPOST_HOSTNAME\"" >&2
echo 'Example: SELFPOST_HOSTNAME=mail.example.com' >&2
exit 1
;;
*.*)
;;
*)
echo "FATAL: SELFPOST_HOSTNAME must be a fully-qualified domain name (at least one dot): \"$SELFPOST_HOSTNAME\"" >&2
echo 'Example: SELFPOST_HOSTNAME=mail.example.com' >&2
exit 1
;;
esac
# The persistent root /data is a host bind mount, so it arrives owned by the
# host user (typically root), not by the unprivileged panel user that actually
# writes the SQLite database, setup token and DKIM keys. Fix its ownership here
# — the one place still running as root — before handing off to supervisord,
# which starts the panel as the panel user.
#
# Mode must stay world-traversable (0755): OpenDKIM and Postfix reach their
# trees under /data as other users. Go's testing.TempDir is 0700, and a bare
# chown would leave that mode in place — opendkim then cannot read KeyTable and
# the container crash-loops (e2e TestHostnameGate/valid_hostname_starts).
chown panel:panel /data
chmod 755 /data
# Restored backups or previously-created state may contain panel-owned files
# under /data; make sure they stay writable without disturbing anything that a
# later phase deliberately hands to another service. /data/log is exempt: it is
# deliberately owned by postfix (postlogd writes the delivery log there) and is
# normalised on its own below.
find /data -mindepth 1 -maxdepth 1 ! -user panel ! -name log ! -name postfix -exec chown -R panel:panel {} +
# DKIM key tree. The panel (user `panel`) generates keys and writes
# the OpenDKIM tables; OpenDKIM (user `opendkim`) must read them. Normalise the
# tree on every start so it is correct whether /data is fresh, restarted, or
# just restored from a backup:
# - group `selfpost` + setgid on directories so anything the panel creates
# inherits the shared group OpenDKIM traverses;
# - private keys and tables group-readable (0640);
# - both table files present (empty is fine) BEFORE OpenDKIM starts, so the
# daemon comes up cleanly with no domains yet.
mkdir -p /data/opendkim/keys
for t in /data/opendkim/KeyTable /data/opendkim/SigningTable; do
[ -e "$t" ] || : > "$t"
done
chown -R panel:selfpost /data/opendkim
find /data/opendkim -type d -exec chmod 2750 {} +
chmod 0640 /data/opendkim/KeyTable /data/opendkim/SigningTable
find /data/opendkim/keys -type f -name '*.private' -exec chmod 0640 {} +
# Application SASL accounts. The panel (user `panel`) writes the
# sasldb2 via saslpasswd2; Postfix (user `postfix`) reads it to authenticate SMTP
# clients. Share it through the `selfpost` group the same way as the DKIM tree:
# setgid directory so new files inherit the group, and the database itself
# group-readable (0640).
mkdir -p /data/sasl
chown -R panel:selfpost /data/sasl
chmod 2750 /data/sasl
[ -e /data/sasl/sasldb2 ] && chmod 0640 /data/sasl/sasldb2
# Postfix state under /data (architecture.md § Persistence). The panel writes
# sender_login_maps; Postfix owns the on-disk queue tree under queue/.
mkdir -p /data/postfix/queue
[ -e /data/postfix/sender_login_maps ] || : > /data/postfix/sender_login_maps
# Inbound relay maps (written by the panel when INBOUND_RELAY_ENABLE=true).
# Empty files keep postfix check happy if the flag is on before any domain exists.
for f in relay_domains transport relay_recipients tls_policy; do
[ -e "/data/postfix/$f" ] || : > "/data/postfix/$f"
chown panel:selfpost "/data/postfix/$f"
chmod 0640 "/data/postfix/$f"
done
chown panel:selfpost /data/postfix
chmod 2750 /data/postfix
# Filter lists for the external spam filter (written by the panel when
# INBOUND_RELAY_ENABLE=true, read by the antispam sidecar, which mounts this
# directory). The sidecar runs under a user of its own in another container,
# so the directory and its files are readable by anyone: they hold sender
# addresses and networks, no secrets.
mkdir -p /data/antispam
chown panel:selfpost /data/antispam
chmod 0755 /data/antispam
chown panel:selfpost /data/postfix/sender_login_maps
chmod 0640 /data/postfix/sender_login_maps
# Delivery log (architecture.md § Log tailer). postlogd writes it as user
# `postfix`; the panel reads it for the log-tailer and the System log page. It
# lives under /data — not the ephemeral /var/log — so the delivery lines that
# resolve a "queued" send-log row survive a container recreate.
#
# postlogd creates a missing log itself, but at 0600, which the unprivileged
# panel cannot read; so create it here (and re-normalise an existing one, plus
# whatever logrotate left behind) at 0640 owned postfix:selfpost. The setgid
# directory keeps the shared group on anything created inside it later, and
# 2750 keeps it group-traversable but not group-writable — logrotate refuses to
# rotate a log whose directory is writable by a non-root group.
mkdir -p /data/log
[ -e /data/log/mail.log ] || : > /data/log/mail.log
chown -R postfix:selfpost /data/log
chmod 2750 /data/log
find /data/log -type f -exec chmod 0640 {} +
# Milter socket directories. Postfix (user `postfix`) must actually CONNECT to
# both milter sockets — OpenDKIM's and the panel's journal-milter — not just
# probe them at start-up. The sockets are created by the opendkim and panel
# users respectively, so bridge them to `postfix` through the shared `selfpost`
# group: group-owned + setgid dirs mean each socket created inside inherits
# group `selfpost`, and group-traversable (2750) lets postfix reach it. Without
# this, smtpd cannot talk to OpenDKIM and, because signing is strict
# (default_action=tempfail), rejects all mail.
mkdir -p /run/opendkim /run/selfpost
chown opendkim:selfpost /run/opendkim
chown panel:selfpost /run/selfpost
chmod 2750 /run/opendkim /run/selfpost
# Generate the outbound-relay Postfix configuration from the environment (spec
# 5). Kept out of the image build so cert paths, rate limits, hostname and the
# optional 587 service are all driven by env at run time, and re-derived on every
# start the same way the /data normalisation above is.
/usr/local/bin/postfix-config.sh
# Initialise the persistent queue tree on first start or after restore. postfix
# set-permissions reads queue_directory from main.cf (set by postfix-config.sh).
if [ ! -d /data/postfix/queue/active ]; then
postfix set-permissions
fi
chown -R postfix:postfix /data/postfix/queue
# The recursive chown above would leave public/ and maildrop/ in group postfix.
# Postfix keeps those two in its setgid group: postqueue and postdrop are setgid
# postdrop and reach the showq socket and the maildrop directory through that
# group alone. With the wrong group `postqueue -p` run by the panel answers
# "Connect to the Postfix showq service: Permission denied" and the panel cannot
# read the queue (the Queue card and the Outbound queue page).
chgrp postdrop /data/postfix/queue/public /data/postfix/queue/maildrop
# It would also leave the queue directory itself and pid/ owned by postfix.
# Postfix keeps those two owned by root (master, which runs as root, is the only
# writer) and logs "warning: not owned by root" for each at every start.
chown root /data/postfix/queue /data/postfix/queue/pid
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf