release: cut 1.0.0

Pin compose and local trial to ghcr.io/mixeme/selfpost:1.0.0, close the
CHANGELOG cut, and retire implementation-plan / v1.x-closure-plan.

Includes the post-cut startup fixes needed for a green release e2e gate:
root-owned TLS copies for postfix check, maillog_file_prefixes for /data,
hostname gate and traversable /data, panel /healthz before setup, and
setup-token / TempDir reclaim via docker exec.

Co-Authored-By: Composer <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mixeme
2026-08-09 12:33:29 +03:00
parent 18880a8286
commit 178424eaf6
20 changed files with 284 additions and 440 deletions
+57 -47
View File
@@ -1,14 +1,69 @@
#!/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 (spec 5.2 p.3, 8). 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 (spec 9), 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 (spec 7.6.8).
# Fix its ownership here — the one place still running as root — before handing
# off to supervisord, which starts the panel as the panel user.
set -e
#
# 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
@@ -83,51 +138,6 @@ chown opendkim:selfpost /run/opendkim
chown panel:selfpost /run/selfpost
chmod 2750 /run/opendkim /run/selfpost
# 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 (spec 5.2 p.3, 8). 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
# 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
+38 -3
View File
@@ -23,8 +23,26 @@ 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_CERT="${TLS_CERT_FILE:-/etc/postfix/tls/fullchain.pem}"
TLS_KEY="${TLS_KEY_FILE:-/etc/postfix/tls/privkey.pem}"
#
# 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
# copy into a writable internal dir and normalise ownership before postconf
# and `postfix check`.
TLS_CERT_SRC="${TLS_CERT_FILE:-/etc/postfix/tls/fullchain.pem}"
TLS_KEY_SRC="${TLS_KEY_FILE:-/etc/postfix/tls/privkey.pem}"
TLS_INTERNAL_DIR=/etc/postfix/tls-internal
TLS_CERT="$TLS_CERT_SRC"
TLS_KEY="$TLS_KEY_SRC"
if [ -f "$TLS_CERT_SRC" ] && [ -f "$TLS_KEY_SRC" ]; then
mkdir -p "$TLS_INTERNAL_DIR"
cp -f "$TLS_CERT_SRC" "$TLS_INTERNAL_DIR/fullchain.pem"
cp -f "$TLS_KEY_SRC" "$TLS_INTERNAL_DIR/privkey.pem"
chown root:root "$TLS_INTERNAL_DIR/fullchain.pem" "$TLS_INTERNAL_DIR/privkey.pem"
chmod 0644 "$TLS_INTERNAL_DIR/fullchain.pem"
chmod 0600 "$TLS_INTERNAL_DIR/privkey.pem"
TLS_CERT="$TLS_INTERNAL_DIR/fullchain.pem"
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).
@@ -54,14 +72,23 @@ SUBMISSION_ENABLE="${SUBMISSION_ENABLE:-false}"
MAIL_LOG_PATH="${MAIL_LOG:-/data/log/mail.log}"
# --- main.cf -----------------------------------------------------------------
# maillog_file lives under the persistent /data bind mount (architecture.md §
# Log tailer). Postfix's default maillog_file_prefixes are only /var and
# /dev/stdout — without /data, `postfix check` rejects the path.
postconf -e \
"myhostname=${HOSTNAME_VALUE}" \
"maillog_file=${MAIL_LOG_PATH}" \
"maillog_file_prefixes=/var,/dev/stdout,/data" \
"mydestination=" \
"relayhost=" \
"inet_interfaces=all" \
"inet_protocols=all"
# postlogd is mandatory whenever maillog_file is set (MAILLOG_README). Debian's
# stock master.cf usually has it; pin it explicitly so a stripped/upgraded
# image cannot lose the service.
postconf -M "postlog/unix-dgram=postlog unix-dgram n - n - 1 postlogd"
# This is an outbound relay: no local delivery, no per-user aliases. Empty
# these so a misfiled recipient never gets delivered locally.
postconf -e \
@@ -189,4 +216,12 @@ EOF
# Validate the generated configuration; fail loudly if postconf produced
# anything Postfix rejects, before the wrapper tries to start it.
postfix check
set +e
check_out=$(postfix check 2>&1)
ec=$?
set -e
if [ "$ec" -ne 0 ]; then
echo "postfix-config: postfix check failed exit $ec" >&2
printf '%s\n' "$check_out" >&2
exit "$ec"
fi