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.
This commit is contained in:
xavor 2026-05-20 20:25:04 +00:00
commit 9e21e1e669
7 changed files with 288 additions and 0 deletions

22
Dockerfile Normal file
View File

@ -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"]

63
README.md Normal file
View File

@ -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).

103
build.sh Normal file
View File

@ -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 <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
namespace: kaniko
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: Never
imagePullSecrets:
- name: harbor-pull-secret
initContainers:
- name: fetch-context
image: harbor.manabo.org/library/minio/mc:RELEASE.2025-08-13T08-35-41Z
command: ["/bin/sh", "-c"]
args:
- |
mc alias set minio \$MINIO_ENDPOINT \$MC_ACCESS_KEY \$MC_SECRET_KEY --api S3v4 &&
mc cp minio/${BUCKET}/${CONTEXT_KEY} /context/context.tar.gz
env:
- name: MC_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-kaniko-creds
key: access-key
- name: MC_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-kaniko-creds
key: secret-key
- name: MINIO_ENDPOINT
valueFrom:
secretKeyRef:
name: minio-kaniko-creds
key: endpoint
volumeMounts:
- name: context
mountPath: /context
containers:
- name: kaniko
image: harbor.manabo.org/gcr/kaniko-project/executor:v1.23.2
args:
- "--context=tar:///context/context.tar.gz"
- "--destination=${IMAGE}"
- "--snapshot-mode=redo"
- "--log-format=text"
volumeMounts:
- name: context
mountPath: /context
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: context
emptyDir: {}
- name: docker-config
secret:
secretName: harbor-push-config
items:
- key: .dockerconfigjson
path: config.json
EOF
echo "==> 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}"

34
entrypoint.sh Normal file
View File

@ -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

27
main.cf Normal file
View File

@ -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

36
master.cf Normal file
View File

@ -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

3
smtpd.conf Normal file
View File

@ -0,0 +1,3 @@
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN