release: 1.9.2: Review pass: port-25 milters, DMARC validation, rate-limit fix
Review pass over `1.4.0`…`1.9.1`: port 25 no longer shares the submission milter chain, DMARC reports are validated before they are stored, and the auto rate limit is correct for a non-default level-1 window. Schema migration `0010` (index only). Upgrading is a tag bump. Added: - SQLite migration `0010_send_log_app_login_index.sql` — - DMARC aggregate reports are accepted as **zip** as well as gzip or plain XML - e2e: `checkPort25ConfigCombinations` regenerates the Postfix configuration Changed: - README hero line and a **Positioning** block in `docs/product.md` record the Fixed: - The setup account's password is hashed at bcrypt cost `12` like every other - Login/setup rate limiter: protecting blocked buckets from eviction is now - `postqueue -p` output is bounded to 4 MiB (the timeout alone left the size of - [schema-migrations.md](docs/schema-migrations.md): the schema section was - postqueue reader is now time-bounded: `postqueue -p` runs under a 5-second … (11 more in CHANGELOG) Changed: - [roadmap.md](docs/roadmap.md) now carries everything the 2026-08-19 code - Extracted `internal/configsafe` — the empty-value and forbidden-character - Build, deploy and migration comments no longer cite the archived - Dropped the `parseDomainRateLimitForm` / `parseAppRateLimitForm` pass-through - Extracted shared helpers: `internal/atomicfile` (atomic temp-write+rename) and … (7 more in CHANGELOG) Removed: - Unused legacy asset `docs/assets/icon.png` (superseded by the four brand SVGs - Plan documents for already-shipped features: `domain-stats-auto-ratelimit`, Full detail: CHANGELOG.md (1.9.2).
This commit is contained in:
+57
-53
@@ -1,16 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Generate the outbound-relay Postfix configuration (spec 5, 5.1, 5.2).
|
||||
# Generate the outbound-relay Postfix configuration.
|
||||
#
|
||||
# Run as root from entrypoint.sh on every container start, BEFORE supervisord
|
||||
# starts the Postfix wrapper. Like the rest of SelfPost's runtime state it is
|
||||
# re-derived from the environment on each start rather than persisted, so a
|
||||
# fresh, restarted or restored container always ends up with the same config
|
||||
# (the only panel-edited Postfix file, sender_login_maps, lives under /data and
|
||||
# is generated separately by the panel — spec 5.1).
|
||||
# is generated separately by the panel).
|
||||
#
|
||||
# main.cf is written with `postconf -e`, the master.cf submission services with
|
||||
# `postconf -M`/`-P`. No user input is interpolated: every value here comes from
|
||||
# a fixed literal or a container environment variable (spec 7.6.3).
|
||||
# a fixed literal or a container environment variable.
|
||||
set -eu
|
||||
|
||||
# postconf -P refuses master.cf -o values that contain whitespace; inbound port
|
||||
@@ -44,16 +44,16 @@ master_cf_set_smtp_inet_o() {
|
||||
mv /etc/postfix/master.cf.new /etc/postfix/master.cf
|
||||
}
|
||||
|
||||
# --- environment knobs (spec 8) ----------------------------------------------
|
||||
# --- environment knobs -------------------------------------------------------
|
||||
# Server hostname: used as HELO name AND, crucially, as the Cyrus SASL realm the
|
||||
# sasldb2 accounts are looked up under. The panel creates accounts under realm
|
||||
# $SELFPOST_HOSTNAME (SASL_REALM), so myhostname MUST match or authentication
|
||||
# fails. Fall back to the container hostname only outside a real deployment.
|
||||
HOSTNAME_VALUE="${SELFPOST_HOSTNAME:-$(hostname -f 2>/dev/null || hostname)}"
|
||||
|
||||
# TLS material supplied by the reverse-proxy through a read-only bind mount
|
||||
# (spec 5.2). The relay requires TLS on 465; if these files are absent the
|
||||
# master still starts but TLS handshakes on 465 fail until they appear.
|
||||
# TLS material supplied by the reverse-proxy through a read-only bind mount. The
|
||||
# relay requires TLS on 465; if these files are absent the master still starts
|
||||
# but TLS handshakes on 465 fail until they appear.
|
||||
#
|
||||
# Postfix insists the private key is root-owned and mode 0600. The bind mount
|
||||
# is often :ro and owned by the host user (e2e TempDir / CI runner UID), so
|
||||
@@ -75,8 +75,8 @@ if [ -f "$TLS_CERT_SRC" ] && [ -f "$TLS_KEY_SRC" ]; then
|
||||
TLS_KEY="$TLS_INTERNAL_DIR/privkey.pem"
|
||||
fi
|
||||
|
||||
# Level-1 rate limit (native Postfix anvil, spec 5 p.5 / 7.4). Conservative
|
||||
# defaults, sensible during IP warm-up (spec 10).
|
||||
# Level-1 rate limit (native Postfix anvil). Conservative defaults, sensible
|
||||
# during IP warm-up.
|
||||
RATE_MSGS="${RATE_LIMIT_MESSAGES_PER_IP:-100}"
|
||||
RATE_WINDOW="${RATE_LIMIT_WINDOW_SECONDS:-3600}"
|
||||
|
||||
@@ -85,16 +85,16 @@ RATE_WINDOW="${RATE_LIMIT_WINDOW_SECONDS:-3600}"
|
||||
OPENDKIM_SOCK="${OPENDKIM_SOCKET:-/run/opendkim/opendkim.sock}"
|
||||
JOURNAL_SOCK="${JOURNAL_MILTER_SOCKET:-/run/selfpost/journal.sock}"
|
||||
|
||||
# Persistent panel-managed sender map (spec 5.1); texthash needs no postmap, so
|
||||
# the unprivileged panel can rewrite it and just ask for a reload.
|
||||
# Persistent panel-managed sender map; texthash needs no postmap, so the
|
||||
# unprivileged panel can rewrite it and just ask for a reload.
|
||||
SENDER_LOGIN_MAPS="${POSTFIX_SENDER_LOGIN_MAPS:-/data/postfix/sender_login_maps}"
|
||||
# Transit mail queue under /data so deferred/active messages survive container
|
||||
# recreate (architecture.md § Persistence). Distinct from sender_login_maps.
|
||||
QUEUE_DIR="${POSTFIX_QUEUE_DIR:-/data/postfix/queue}"
|
||||
SASLDB_PATH="${SASL_DB_PATH:-/data/sasl/sasldb2}"
|
||||
|
||||
# Optional submission service on 587 (spec 5 p.1: off by default, enabled only
|
||||
# when a client library needs STARTTLS on 587 instead of implicit TLS on 465).
|
||||
# Optional submission service on 587 (off by default, enabled only when a client
|
||||
# library needs STARTTLS on 587 instead of implicit TLS on 465).
|
||||
SUBMISSION_ENABLE="${SUBMISSION_ENABLE:-false}"
|
||||
|
||||
# Optional inbound relay (backup-MX / forwarder). Off by default: port 25 does
|
||||
@@ -153,13 +153,13 @@ postconf -e \
|
||||
"alias_maps=" \
|
||||
"alias_database="
|
||||
|
||||
# Outbound delivery: straight to the recipient MX, opportunistic TLS (spec 5 p.2).
|
||||
# Outbound delivery: straight to the recipient MX, opportunistic TLS.
|
||||
postconf -e \
|
||||
"smtp_tls_security_level=may" \
|
||||
"smtp_tls_note_starttls_offer=yes"
|
||||
|
||||
# TLS server material shared by every inbound service (spec 5.2). auth_only
|
||||
# guarantees credentials are never accepted before TLS is up on any port.
|
||||
# TLS server material shared by every inbound service. auth_only guarantees
|
||||
# credentials are never accepted before TLS is up on any port.
|
||||
postconf -e \
|
||||
"smtpd_tls_cert_file=${TLS_CERT}" \
|
||||
"smtpd_tls_key_file=${TLS_KEY}" \
|
||||
@@ -167,11 +167,11 @@ postconf -e \
|
||||
"smtpd_tls_auth_only=yes" \
|
||||
"smtpd_tls_loglevel=1"
|
||||
|
||||
# SASL: Cyrus with the local sasldb2 the panel maintains (spec 5.1). The realm
|
||||
# is left implicit (smtpd_sasl_local_domain empty) so the authenticated name
|
||||
# Postfix uses for sender_login_maps is the BARE login the panel writes into the
|
||||
# map; the sasldb2 lookup still resolves because Postfix hands Cyrus $myhostname
|
||||
# as the server realm, which equals the realm the accounts were created under.
|
||||
# SASL: Cyrus with the local sasldb2 the panel maintains. The realm is left
|
||||
# implicit (smtpd_sasl_local_domain empty) so the authenticated name Postfix
|
||||
# uses for sender_login_maps is the BARE login the panel writes into the map;
|
||||
# the sasldb2 lookup still resolves because Postfix hands Cyrus $myhostname as
|
||||
# the server realm, which equals the realm the accounts were created under.
|
||||
postconf -e \
|
||||
"smtpd_sasl_auth_enable=yes" \
|
||||
"smtpd_sasl_type=cyrus" \
|
||||
@@ -181,43 +181,42 @@ postconf -e \
|
||||
"smtpd_sasl_tls_security_options=noanonymous" \
|
||||
"broken_sasl_auth_clients=yes"
|
||||
|
||||
# Sender binding (spec 5.1 p.3, the critical anti-spoofing control). texthash
|
||||
# resolves the full address first, then the "@domain" wildcard, so both address
|
||||
# modes work from the same map.
|
||||
# Sender binding (the critical anti-spoofing control). texthash resolves the
|
||||
# full address first, then the "@domain" wildcard, so both address modes work
|
||||
# from the same map.
|
||||
postconf -e \
|
||||
"smtpd_sender_login_maps=texthash:${SENDER_LOGIN_MAPS}" \
|
||||
"queue_directory=${QUEUE_DIR}"
|
||||
|
||||
# Restrictions: authenticated clients only, no relay to foreign destinations,
|
||||
# and every authenticated sender address must be owned by its login. NO
|
||||
# permit_mynetworks anywhere — authorisation is by credentials, never by network
|
||||
# (spec 5 p.1/p.4, 5.1). This is what makes an open relay impossible.
|
||||
# permit_mynetworks anywhere — authorisation is by credentials, never by
|
||||
# network. This is what makes an open relay impossible.
|
||||
postconf -e \
|
||||
"smtpd_helo_required=yes" \
|
||||
"smtpd_relay_restrictions=permit_sasl_authenticated, reject_unauth_destination" \
|
||||
"smtpd_recipient_restrictions=permit_sasl_authenticated, reject_unauth_destination" \
|
||||
"smtpd_sender_restrictions=reject_sender_login_mismatch, permit"
|
||||
|
||||
# Level-1 rate limit by client IP (spec 5 p.5). Backstop that keeps working even
|
||||
# if the journal-milter (level 2) is down.
|
||||
# Level-1 rate limit by client IP. Backstop that keeps working even if the
|
||||
# journal-milter (level 2) is down.
|
||||
postconf -e \
|
||||
"smtpd_client_message_rate_limit=${RATE_MSGS}" \
|
||||
"anvil_rate_time_unit=${RATE_WINDOW}s"
|
||||
|
||||
# Milter chain (spec 5 p.3, 7.3). OpenDKIM signs and is treated strictly
|
||||
# (default_action=tempfail: if it is unreachable, defer rather than send
|
||||
# unsigned). The journal-milter is monitoring only and is fail-open
|
||||
# (default_action=accept): its failure must never block the relay. Per-milter
|
||||
# settings use Postfix 3.0+ brace syntax.
|
||||
# Milter chain. OpenDKIM signs and is treated strictly (default_action=tempfail:
|
||||
# if it is unreachable, defer rather than send unsigned). The journal-milter is
|
||||
# monitoring only and is fail-open (default_action=accept): its failure must
|
||||
# never block the relay. Per-milter settings use Postfix 3.0+ brace syntax.
|
||||
postconf -e \
|
||||
"milter_protocol=6" \
|
||||
"milter_default_action=tempfail" \
|
||||
"smtpd_milters={ unix:${OPENDKIM_SOCK}, default_action=tempfail }, { unix:${JOURNAL_SOCK}, default_action=accept }" \
|
||||
"non_smtpd_milters="
|
||||
|
||||
# Bounded milter timeouts (spec 7.3): a *hung* milter (socket accepts but never
|
||||
# replies) must fail open just like a crash, not stall mail acceptance until the
|
||||
# Postfix defaults (300s content) elapse. With default_action per milter, a
|
||||
# Bounded milter timeouts: a *hung* milter (socket accepts but never replies)
|
||||
# must fail open just like a crash, not stall mail acceptance until the Postfix
|
||||
# defaults (300s content) elapse. With default_action per milter, a
|
||||
# journal-milter hang then resolves to accept and an OpenDKIM hang to tempfail,
|
||||
# but within seconds rather than minutes. Values are well above any healthy
|
||||
# response time (signing/DB insert are sub-second), so they never fire in normal
|
||||
@@ -228,8 +227,8 @@ postconf -e \
|
||||
"milter_content_timeout=${MILTER_CONTENT_TIMEOUT:-30s}"
|
||||
|
||||
# --- master.cf: inbound submission services ----------------------------------
|
||||
# smtps (465, implicit/wrapper TLS) — the primary, always-on submission service
|
||||
# (spec 5 p.1). chroot=n so smtpd can read the sasldb2 and sender map under /data
|
||||
# smtps (465, implicit/wrapper TLS) — the primary, always-on submission
|
||||
# service. chroot=n so smtpd can read the sasldb2 and sender map under /data
|
||||
# and the Cyrus config outside any chroot.
|
||||
postconf -M "smtps/inet=smtps inet n - n - - smtpd"
|
||||
postconf -P \
|
||||
@@ -237,10 +236,10 @@ postconf -P \
|
||||
"smtps/inet/smtpd_sasl_auth_enable=yes" \
|
||||
"smtps/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject"
|
||||
|
||||
# submission (587, STARTTLS) — optional (spec 5 p.1). Same SASL/milter/limits;
|
||||
# the only difference is TLS is negotiated via STARTTLS, so require encryption
|
||||
# before auth. Added only when SUBMISSION_ENABLE=true, otherwise removed so a
|
||||
# restart after disabling it does not leave the port listening.
|
||||
# submission (587, STARTTLS) — optional. Same SASL/milter/limits; the only
|
||||
# difference is TLS is negotiated via STARTTLS, so require encryption before
|
||||
# auth. Added only when SUBMISSION_ENABLE=true, otherwise removed so a restart
|
||||
# after disabling it does not leave the port listening.
|
||||
if [ "${SUBMISSION_ENABLE}" = "true" ]; then
|
||||
postconf -M "submission/inet=submission inet n - n - - smtpd"
|
||||
postconf -P \
|
||||
@@ -301,6 +300,13 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
"smtp_tls_policy_maps="
|
||||
fi
|
||||
|
||||
# Milters on port 25. main.cf's chain (OpenDKIM strict + journal-milter) is
|
||||
# for the SUBMISSION path only: OpenDKIM must not gate someone else's
|
||||
# inbound mail (a signing outage would defer it), and the journal-milter
|
||||
# would file inbound mail into the outbound send log under the *sender's*
|
||||
# domain — polluting Deliveries and letting a forged From: consume that
|
||||
# domain's level-2 rate-limit budget. So smtpd_milters is always overridden
|
||||
# here: either the optional antispam milter, or nothing at all.
|
||||
INBOUND_MILTERS=""
|
||||
if [ "${INBOUND_RELAY_ENABLE}" = "true" ] && [ -n "${INBOUND_ANTISPAM_MILTER}" ]; then
|
||||
case "${INBOUND_ANTISPAM_MILTER}" in
|
||||
@@ -346,9 +352,7 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
"smtp/inet/smtpd_client_message_rate_limit=${PORT25_RATE}" \
|
||||
"smtp/inet/message_size_limit=${PORT25_SIZE}"
|
||||
master_cf_set_smtp_inet_o smtpd_recipient_restrictions "${RECIPIENT_RESTRICTIONS}"
|
||||
if [ -n "${INBOUND_MILTERS}" ]; then
|
||||
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
||||
fi
|
||||
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
||||
else
|
||||
postconf -MX "smtp/inet" 2>/dev/null || true
|
||||
postconf -MX "dmarc-ingest/unix-pipe" 2>/dev/null || true
|
||||
@@ -359,18 +363,18 @@ else
|
||||
"smtp_tls_policy_maps="
|
||||
fi
|
||||
|
||||
# Disable chroot for every service (spec 5 p.2). Debian ships the smtp delivery
|
||||
# agent and others chrooted to /var/spool/postfix, where they cannot read
|
||||
# /etc/resolv.conf — so outbound MX lookups fail with "Host not found" and mail
|
||||
# never leaves. Inside a container the chroot buys little (the container is the
|
||||
# isolation boundary) and breaks DNS/TLS trust-store access, so turn it off
|
||||
# uniformly. Our own smtps/submission services are already n; this covers the
|
||||
# delivery agents and the rest.
|
||||
# Disable chroot for every service. Debian ships the smtp delivery agent and
|
||||
# others chrooted to /var/spool/postfix, where they cannot read /etc/resolv.conf
|
||||
# — so outbound MX lookups fail with "Host not found" and mail never leaves.
|
||||
# Inside a container the chroot buys little (the container is the isolation
|
||||
# boundary) and breaks DNS/TLS trust-store access, so turn it off uniformly. Our
|
||||
# own smtps/submission services are already n; this covers the delivery agents
|
||||
# and the rest.
|
||||
postconf -F "*/*/chroot=n"
|
||||
|
||||
# --- Cyrus SASL app config for smtpd -----------------------------------------
|
||||
# Tells the Cyrus library (invoked by smtpd via smtpd_sasl_path=smtpd) to verify
|
||||
# passwords straight from the panel-maintained sasldb2 (spec 5.1). PLAIN/LOGIN
|
||||
# passwords straight from the panel-maintained sasldb2. PLAIN/LOGIN
|
||||
# only — both are safe because TLS is mandatory before auth on every port.
|
||||
mkdir -p /etc/postfix/sasl
|
||||
cat > /etc/postfix/sasl/smtpd.conf <<EOF
|
||||
|
||||
Reference in New Issue
Block a user