From 9e21e1e66923bc1ec266d43b08497d59662a4c14 Mon Sep 17 00:00:00 2001 From: xavor Date: Wed, 20 May 2026 20:25:04 +0000 Subject: [PATCH] feat(smtp-relay): initial custom postfix+sasl relay image Postfix relay image with Cyrus SASL (sasldb2) authentication. Replaces mwader/postfix-relay with a controlled image built via Kaniko and stored in Harbor. Credentials injected from Vault ExternalSecret at startup. --- Dockerfile | 22 +++++++++++ README.md | 63 ++++++++++++++++++++++++++++++ build.sh | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ entrypoint.sh | 34 +++++++++++++++++ main.cf | 27 +++++++++++++ master.cf | 36 ++++++++++++++++++ smtpd.conf | 3 ++ 7 files changed, 288 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 build.sh create mode 100644 entrypoint.sh create mode 100644 main.cf create mode 100644 master.cf create mode 100644 smtpd.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46c9369 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM debian:12-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + postfix \ + ca-certificates \ + sasl2-bin \ + libsasl2-modules \ + netcat-openbsd \ + && rm -rf /var/lib/apt/lists/* \ + && (getent group postdrop || groupadd -r postdrop) \ + && usermod -aG postdrop postfix + +COPY main.cf /etc/postfix/main.cf +COPY master.cf /etc/postfix/master.cf +COPY smtpd.conf /etc/postfix/sasl/smtpd.conf +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 25 587 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..60e0833 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# smtp-relay + +Postfix SMTP relay with Cyrus SASL authentication and TLS. + +Used by Mailu (personal + Solidaria NGO) on valhalla to route outbound mail through +hermes, which has a trusted residential IP accepted by Gmail and Hotmail. + +Image: `harbor.manabo.org/library/smtp-relay` +Deployed on: hermes (`clusters/hermes/smtp-relay/` in asgard) + +--- + +## Build + +```bash +./build.sh 1.0.0 +``` + +Packages the Dockerfile context, uploads to MinIO, runs Kaniko in-cluster on valhalla, +and pushes the resulting image to Harbor. + +--- + +## Configuration + +### Required Vault secrets (`app/smtp-relay/smtp-relay-sasl`) + +| Key | Description | +|-----|-------------| +| `relay_user` | SASL username (e.g. `relayuser`) | +| `relay_pass` | SASL password (plaintext — stored in Vault) | +| `relay_domain` | SASL domain (e.g. `manabo.org`) | + +### TLS (`certs/smtp-relay-tls`) + +Wildcard cert for `relay.manabo.org` — pushed to Vault via PushSecret on valhalla. + +### Env vars (from ExternalSecret) + +| Var | Source | +|-----|--------| +| `RELAY_AUTH_USER` | `relay_user` | +| `RELAY_AUTH_PASS` | `relay_pass` | +| `RELAY_AUTH_DOMAIN` | `relay_domain` | + +--- + +## How it works + +- Listens on ports **25** (SMTP, TLS optional) and **587** (submission, TLS required) +- Uses `hostNetwork: true` — ports exposed directly on the hermes host IP +- Entrypoint creates a `sasldb2` user from the env vars on every start +- Only clients authenticated via SASL can relay mail +- TLS cert mounted from Vault ExternalSecret + +--- + +## Gotchas + +- **sasldb2 recreated on every restart**: credentials are read from env vars and + `saslpasswd2` re-creates the sasldb. This is intentional (stateless SASL). +- **No DKIM**: DKIM signing is not implemented in this image. Relay delivers mail + as-is; DKIM signatures must be added by the sending MTA (Mailu). diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..2bec8cb --- /dev/null +++ b/build.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# build.sh [tag] +# Packages the smtp-relay context, uploads to MinIO, runs Kaniko in-cluster, waits. +set -euo pipefail + +TAG="${1:-latest}" +HARBOR="harbor.manabo.org" +IMAGE="${HARBOR}/library/smtp-relay:${TAG}" +BUCKET="kaniko-builds" +CONTEXT_KEY="smtp-relay/context.tar.gz" + +echo "==> Building ${IMAGE}" + +echo "==> Packaging context ..." +tar -czf /tmp/kaniko-context.tar.gz \ + --exclude='.git' \ + --exclude='build.sh' \ + --exclude='k8s' \ + -C "$(dirname "$0")" . +echo "==> Uploading to MinIO (${BUCKET}/${CONTEXT_KEY}) ..." +mc cp /tmp/kaniko-context.tar.gz "minio/${BUCKET}/${CONTEXT_KEY}" +rm /tmp/kaniko-context.tar.gz + +JOB_NAME="kaniko-smtp-relay-$(date +%s)" +echo "==> Launching Kaniko job: ${JOB_NAME}" + +cat < Waiting for build (timeout 10m) ..." +kubectl wait "job/${JOB_NAME}" -n kaniko \ + --for=condition=complete \ + --timeout=600s || { + echo "==> Build FAILED. Logs:" + POD=$(kubectl get pods -n kaniko -l "job-name=${JOB_NAME}" -o name | head -1) + kubectl logs -n kaniko "$POD" --all-containers + kubectl delete "job/${JOB_NAME}" -n kaniko --ignore-not-found + exit 1 +} + +echo "==> Done: ${IMAGE}" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..9dea1ab --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Required (from ExternalSecret) +: "${RELAY_AUTH_USER:?Missing RELAY_AUTH_USER}" +: "${RELAY_AUTH_PASS:?Missing RELAY_AUTH_PASS}" +: "${RELAY_AUTH_DOMAIN:?Missing RELAY_AUTH_DOMAIN}" + +# Optional hostname override +if [ -n "${POSTFIX_MYHOSTNAME:-}" ]; then + echo "${POSTFIX_MYHOSTNAME}" > /etc/mailname + postconf -e "myhostname=${POSTFIX_MYHOSTNAME}" +fi + +# Create SASL user in sasldb2 +echo "${RELAY_AUTH_PASS}" | saslpasswd2 -p -c -u "${RELAY_AUTH_DOMAIN}" "${RELAY_AUTH_USER}" + +chown root:sasl /etc/sasldb2 2>/dev/null || true +chmod 640 /etc/sasldb2 2>/dev/null || true + +# Postfix spool directories (required inside container) +mkdir -p /var/spool/postfix /var/lib/postfix +chown root:root /var/spool/postfix +chmod 755 /var/spool/postfix + +mkdir -p /var/spool/postfix/public /var/spool/postfix/maildrop +chown root:postdrop /var/spool/postfix/public /var/spool/postfix/maildrop +chmod 1730 /var/spool/postfix/public /var/spool/postfix/maildrop + +mkdir -p /var/spool/postfix/pid +chown root:root /var/spool/postfix/pid +chmod 755 /var/spool/postfix/pid + +exec postfix start-fg diff --git a/main.cf b/main.cf new file mode 100644 index 0000000..eb3cec8 --- /dev/null +++ b/main.cf @@ -0,0 +1,27 @@ +myhostname = relay.manabo.org +myorigin = $myhostname +mydestination = +local_transport = error:local delivery disabled +inet_interfaces = all +inet_protocols = all +mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 + +# SASL authentication (Cyrus SASL / sasldb2) +smtpd_sasl_auth_enable = yes +smtpd_sasl_type = cyrus +smtpd_sasl_path = smtpd +smtpd_sasl_security_options = noanonymous +smtpd_sasl_local_domain = $myhostname +cyrus_sasl_config_path = /etc/postfix/sasl + +# Only accept mail from authenticated clients +smtpd_relay_restrictions = permit_sasl_authenticated, reject +smtpd_recipient_restrictions = permit_sasl_authenticated, reject_unauth_destination + +# TLS (cert mounted from ExternalSecret) +smtpd_tls_cert_file = /etc/postfix/tls/tls.crt +smtpd_tls_key_file = /etc/postfix/tls/tls.key +smtpd_tls_security_level = may +smtpd_tls_auth_only = yes +smtpd_tls_loglevel = 1 +smtp_tls_security_level = may diff --git a/master.cf b/master.cf new file mode 100644 index 0000000..d07a0f2 --- /dev/null +++ b/master.cf @@ -0,0 +1,36 @@ +# Postfix master process configuration +# service type private unpriv chroot wakeup maxproc command + args + +# Port 25 — SMTP (TLS optional, SASL required to relay) +smtp inet n - y - - smtpd + +# Port 587 — Submission (TLS required, SASL required) +submission inet n - y - - smtpd + -o smtpd_tls_security_level=encrypt + -o smtpd_tls_auth_only=yes + -o smtpd_sasl_auth_enable=yes + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + -o smtpd_tls_loglevel=1 + +pickup unix n - y 60 1 pickup +cleanup unix n - y - 0 cleanup +qmgr unix n - n 300 1 qmgr +rewrite unix - - y - - trivial-rewrite +bounce unix - - y - 0 bounce +defer unix - - y - 0 bounce +trace unix - - y - 0 bounce +verify unix - - y - 1 verify +flush unix n - y 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - y - - smtp +relay unix - - y - - smtp +showq unix n - y - - showq +error unix - - y - - error +retry unix - - y - - error +discard unix - - y - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - y - - lmtp +anvil unix - - y - 1 anvil +scache unix - - y - 1 scache diff --git a/smtpd.conf b/smtpd.conf new file mode 100644 index 0000000..b89550c --- /dev/null +++ b/smtpd.conf @@ -0,0 +1,3 @@ +pwcheck_method: auxprop +auxprop_plugin: sasldb +mech_list: PLAIN LOGIN