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:
commit
9e21e1e669
22
Dockerfile
Normal file
22
Dockerfile
Normal 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
63
README.md
Normal 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
103
build.sh
Normal 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
34
entrypoint.sh
Normal 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
27
main.cf
Normal 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
36
master.cf
Normal 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
3
smtpd.conf
Normal file
@ -0,0 +1,3 @@
|
||||
pwcheck_method: auxprop
|
||||
auxprop_plugin: sasldb
|
||||
mech_list: PLAIN LOGIN
|
||||
Loading…
Reference in New Issue
Block a user