SMTP-Relay in Docker nutzen
Für die Webanwendungen in meinem Docker-Setup möchte ich Mailversand ermöglichen. Damit meine E-Mails nicht überall als Spam abgelehnt werden, will ich sie nicht über die IP-Adresse meiner privaten Internetleitung versenden. Das geht nie gut. Stattdessen möchte ich die Mails über ein Relay im Internet verschicken. Weil ich beim Relay nicht für jede Anwendung eigene Zugangsdaten anlegen möchte, brauche ich eine einfachere Lösung. Ich möchte lokal eine weitere Zwischenstation betreiben, die Mails der Anwendungen in den Containern annimmt und authentifiziert über das Relay im Internet an die Zieladressen schickt.
Zielsetzung ist, einen Container aufzusetzen, in dem Postfix via SMTP Mails annimmt, sich beim Relay-Host im Internet authentifiziert und die Mails darüber dann verschickt. Dabei verzichte ich auf Authentifizierung am lokalen Postfix und regle die Erlaubnis, Mails zu schicken, über die von Docker genutzten IP-Bereiche. Wer in diesem vertrauenswürdigen Netzbereich ist, darf Mails schicken.
Am Ende der Anleitung kann ein Container lokal gebaut, über eine Datei mit Umgebungsvariablen konfiguriert und mit docker-compose
gestartet werden. Es gibt auch ein github-Repository mit dem Code - konfigurieren sollte man den aber schon.
Ausdrücklich nicht geplant ist die Bereitstellung lokaler Mail-Konten. Mails werden in diesem Container nicht gespeichert und können dort auch nicht abgeholt werden. Postfächer müssen woanders gepflegt werden. Hier geht es um reine Weiterleitung.
Voraussetzungen
Gebraucht wird ein Mail-Account bei einem Provider, der SMTP-Authentifizierung erlaubt. GMail benötigt Authentifizierung via OAUTH2, das wird in dieser Anleitung nicht behandelt. Stattdessen kämen etwa Uberspace oder Mailgun in Frage.
Relay-Container
Zum Einsatz kommt Postfix, ein Mail Transfer Agent. Der ist verhältnismäßig einfach zu konfigurieren und funktioniert auch in Docker-Containern. Darunter läuft Debian.
Container bauen - Dockerfile
FROM debian:bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y postfix bsd-mailx
COPY postfix.sh /
RUN chmod +x /postfix.sh
EXPOSE 25
CMD ["/postfix.sh"]
Genutzt wird das slim
-Image von Debian Bullseye (11), installiert wird nichts außer Postfix und einem minimalen Mail-Client für die Kommandozeile, zum testen. Die notwendige Konfiguration von Postfix macht ein Shellscript. Konfigurationsdaten kommen über Umgebungsvariablen hinein.
Postfix konfigurieren und starten - Shellscript
#!/bin/bash
set -eu
echo "Configuring postfix"
echo "${relayhost} ${relayuser}:${relaypassword}" > /etc/postfix/sasl_password
postmap /etc/postfix/sasl_password
postconf -e "inet_protocols = ipv4"
postconf -e "maillog_file = /dev/stdout"
postconf -e "mydestination = localhost"
postconf -e "mydomain = ${mydomain}"
postconf -e "myhostname = ${myhostname:-mail}.${mydomain}"
postconf -e "mynetworks = ${mynetworks:-192.168.0.0/16,172.16.0.0/12}"
postconf -e "myorigin = ${mydomain}"
postconf -e "relayhost = [${relayhost}]:${relayport:-587}"
postconf -e "smtp_host_lookup = native,dns"
postconf -e "smtp_sasl_auth_enable = yes"
postconf -e "smtp_sasl_password_maps = hash:/etc/postfix/sasl_password"
postconf -e "smtp_sasl_security_options = noanonymous"
postconf -e "smtp_use_tls = yes"
echo "nameserver 1.1.1.1" > /var/spool/postfix/etc/resolv.conf
echo "nameserver 1.1.1.1" > /etc/resolv.conf
echo "Starting postfix"
exec /usr/sbin/postfix start-fg
Ein paar Erklärungen:
- Die Zugangsdaten braucht Postfix in einem speziellen Konfigurationsformat,
danach wird nur noch reuläre Konfiguration nach/etc/postfix/main.cf
geschrieben. - Die Konfiguration wird auf IPv4 begrenzt, da IPv6 mit Docker komplizierter ist.
- Persistentes Logfile wird nicht innerhalb des Containers geschrieben, stattdessen in die Log-Ausgabe des Containers.
- Wird
mydestination
auflocalhost
gesetzt, werden alle anderen Zieladressen über das Relay geleitet. - Mailserver brauchen an vielen Stellen Hostnames, deswegen wird einer gesetzt.
- In
mynetworks
wird festgelegt, aus welchen Netzen Mails zur Weiterleitung entgegen genommen werden. Dann wird auch keine weitere Authentifizierung erwartet! - In
myorigin
wird die Domain festgelegt, die ausgehende Mails in derFROM
-Zeile haben sollen. - Der
relayhost
wird mit Port angegeben. Wird kein Port festgelegt, wird587
(submission) angenommen. - Beim DNS-Lookup bedeutet
native
, dass auch Einträge in der/etc/hosts
berücksichtigt werden. Das ist hauptsächlich relevant, wenn die Host-Einträge anderer Container hier hinterlegt wurden. - Die weitere Konfiguration beschäftigt sich mit der Authentifizierung am Relay. Nicht konfigurierbar ist die Verwendung von TLS. Wer das wirklich ausmachen will, kann das gerne hardcoden.
- Um Nameserver nicht durch den (nicht installierten)
systemd-resolved
erreichen zu müssen, werden sie einmal ins System und einmal in das von Postfix genutztechroot
-Verzeichnis hinterlegt.
Am Ende startet das Script Postfix im Vordergrund. Zum einen gehen die Log-Ausgaben dann von stdout
in die Logs des Containers, zum anderen ist der Postfix-Prozess dann der Hauptprozess des Containers. Einen Service Manager wie systemd
oder s6
gibt es nicht.
Konfiguration hinterlegen - Datei mit Umgebungsvariablen
Die Konfiguration des Containers erfolgt mit Umgebungsvariablen. Die können entweder alle auf der Kommandozeile mit docker -e
angegeben werden oder - besser - in einer Datei gespeichert werden, aus der sie beim Starten von docker-compose
gelesen werden.
mydomain=# enter the domain you append to mails here
relayhost=# fqdn for yor relay host
relayuser=# username for your relay host
relaypassword=# password for your relay host
Diese Datei kann nach dem Ausfüllen der Werte dann z.B. mit Dateinamen env
in das gleiche Verzeichnis gespeichert werden.
Container bauen und starten - mit docker-compose
---
version: "3.5"
services:
postfix:
image: postfixdocker
build:
context: .
dockerfile: Dockerfile
env_file: env
networks:
- mail
networks:
mail:
name: mail
Mit dieser docker-compose.yml
kann sowohl das Bauen des Containers als auch dessen Start behandelt werden.
- Zum Bauen:
docker-compose build
Das resultierende Docker-Image wird einfachpostfixdocker
heißen. - Zum Starten
docker-compose up -d
Dazu wird die Datei mit Namenenv
gelesen, um die Umgebungsvariablen des Containers festzulegen. Der Container startet daspostfix.sh
-Script, was mit diesen Variablen dann die lokale Konfigurationsdatei von Postfix schreibt.
Verwendung in anderen Containern
Beim Starten des Postfix-Containers wird auch ein Netzwerk mail
angelegt. Sollen andere Container in der Lage sein, Mails zu schicken, müssen diese ebenfalls Mitglied in diesem Netzwerk sein, um den Postfix-Container erreichen zu können.
Als Minimalbeispiel dient hier ein busybox
-Container in einer fiktiven eigenen docker-compose.yml
:
---
version: "3.5"
services:
busybox:
image: busybox
networks:
- mail
networks:
mail:
external: true
name: mail
Wichtig ist, dass sowohl auf der obersten Ebene das Netzwerk als extern angegeben als auch das Netzwerk ausdrüklich weiter oben dem Container zugewordnet wird. Eine Anwendung in einem so konfigurierten Container kann dann Mails verschicken, wenn sie selbst wie folgt konfiguriert wird:
- SMTP-Host:
mail_postfix_1
(der von `docker-compose automatisch generierte Hostname des Postfix-Containers) - SMTP-Port:
25
(sonst ist keiner exposed, spielt aber auch keine Rolle) - SMTP-Authentifizierung: keine
- SSL/TLS:
StartTLS
oder keine - das ist in Ordnung, alle Container auf dem selben Host laufen und die Kommunikation den einen Host nicht verlässt - SSL-Zertifikat validieren:
off
, da wir kein gültiges Zertifikat fürmail_postfix_1
erzeugt haben.
Logging und Debugging
Logfiles von Postfix fallen einfach im Container an:
$ docker-compose logs
postfix_1 | Sep 20 14:03:52 mail postfix/smtpd[119]: connect from unknown[192.168.32.3]
postfix_1 | Sep 20 14:03:52 mail postfix/smtpd[119]: 4A49CE0A9C: client=unknown[192.168.32.3]
postfix_1 | Sep 20 14:03:52 mail postfix/cleanup[123]: 4A49CE0A9C: message-id=<[email protected]>
postfix_1 | Sep 20 14:03:52 mail postfix/qmgr[117]: 4A49CE0A9C: from=<test@testdomain.de>, size=757, nrcpt=1 (queue active)
postfix_1 | Sep 20 14:03:52 mail postfix/smtpd[119]: disconnect from unknown[192.168.32.3] ehlo=2 starttls=1 mail=1 rcpt=1 data=1 quit=1 commands=7
postfix_1 | Sep 20 14:03:54 mail postfix/smtp[124]: 4A49CE0A9C: to=<[email protected]>, relay=mein_mail_relay.invalid[1.2.3.4]:587, delay=1.9, delays=0.06/0.08/1.7/0.07, dsn=2.0.0, status=sent (250 ok 1632146634 qp 21824)
postfix_1 | Sep 20 14:03:54 mail postfix/qmgr[117]: 4A49CE0A9C: removed
Um eine Mail auszulösen, ohne dafür eigens eine Anwendung konfigurieren zu müssen, kann im Container einfach eine von der Shell aus geschickt werden:
$ docker exec -i -t mail_postfix_1 /bin/bash
mail_postfix_1 $ echo "Testmail" | mail -s "Test-Betreff" [email protected]
Der Erfolg lässt sich in den Logfiles und durch Eingang der Testmail beurteilen.