Howto: Einrichtung von PostTLS unter Debian GNU/Linux „Jessie“

Geschrieben am 30. März 2016

Ende Januar habe ich den Quelltext von PostTLS veröffentlicht und sehr positives Feedback bekommen. In den letzten Tagen habe ich PostTLS auf einem weiteren Produktivserver installiert. Da es noch keine umfassende Dokumentation der für die Einrichtung des Systems notwendigen Schritte gibt, möchte ich hier dokumentieren, welche Schritte in meinem konkreten Fall erforderlich waren.

Es handelte sich um ein ganz einfaches Postfix Relay, welches vor eine Groupware geschaltet ist. Es dient sowohl als MX host als auch als ausgehendes Mail Relay. Als Betriebssystem kommt Debian „Jessie“ zum Einsatz.

Auf geht's.

Einrichtung und Konfiguration einer zweiten Postfix-Instanz

Zur Einrichtung der zweiten Postfix-Instanz wird auf die postmulti-Funktionalität zurück gegriffen. Sofern man damit nicht vertraut ist, rate ich zur Lektüre der entsprechenden Dokumentation (Managing multiple Postfix instances on a single host).

Wir aktivieren Multi Instance Support, sofern nicht bereits geschehen, und erstellen eine neue Instanz, die wir postfix-out-notls nennen.

$ postmulti -e init
$ postmulti -I postfix-out-notls -e create

Ich wurde dabei mit folgender Fehlermeldung konfrontiert:

postfix: warning: dict_open_dlinfo: cannot open /etc/postfix-out-notls/dynamicmaps.cf.  No dynamic maps will be allowed.

Da die Datei dynamicmaps.cf keine spezifischen Inhalte aufweist, löse ich das Problem wie folgt.

$ cp /etc/postfix/dynamicmaps.cf /etc/postfix-out-notls/

Damit haben wir die neue Instanz eingerichtet, aber noch nicht aktiviert (multi_instance_enable = no). Wir können also in Ruhe weiter konfigurieren.

$ postmulti -l
-               -               y         /etc/postfix
postfix-out-notls -               n         /etc/postfix-out-notls

Es sollte folgendes automatisch angelegt worden sein:

  1. Ein Verzeichnis für die Konfiguration der neuen Postfix-Instanz unter /etc/postfix-out-notls/ und
  2. Ein Verzeichnis für die Mail Queue unter /var/spool/postfix-out-notls/.

Bei der Konfiguration der neuen Postfix-Instanz gibt es lediglich zwei Besonderheiten:

Erstens soll der smtpd nicht auf Port 25 lauschen, sondern auf einem anderen Port. Ich habe 10026 verwendet. Dazu muss in der master.cf so etwas stehen:

127.0.0.1:10026     inet  n       -       n       -       -  smtpd

Nun wollen wir, dass diese Instanz Mails auch dann zustellt, wenn der Zielserver kein TLS unterstützt. Wir setzen daher in der main.cf:

smtp_tls_security_level = may

Mit dieser Konfiguration kann die Postfix-Instanz nun aktiviert und gestartet werden:

$ postmulti -i postfix-out-notls -e enable
$ postmulti -i postfix-out-notls -p start

Man wird feststellen, dass der smtpd der zweiten Instanz noch nicht auf Port 10026 erreichbar ist. Das liegt daran, dass postmulti bei der Einrichtung der Instanz folgende Option in der main.cf der zweiten Instanz gesetzt hat:

master_service_disable = inet

Dieser Eintrag ist auszukommentieren. Nach einem Neustart von Postfix sollte der zweite smtpd erreichbar sein:

$ netstat -tulpen | grep 10026
tcp        0      0 127.0.0.1:10026         0.0.0.0:*               LISTEN      0          35899       7093/master

Primäre Postfix-Instanz auf Mandatory TLS umstellen

Die primäre Postfix-Instanz soll nun so konfiguriert werden, dass Mails zurück gehalten werden, wenn der Zielserver kein TLS unterstützt. Dazu setzen wir in der Datei main.cf folgende Option:

smtp_tls_security_level = encrypt

Installation von PostTLS

Ich betreibe PostTLS mit Nutzerrechten und gebe dem Benutzer nur für die benötigten Kommandos die entsprechenden Rechte, diese per sudo aufzurufen.

Benutzer muss Befehle via sudo ausführen können:

$ apt-get install sudo
$ adduser hendrik sudo

Dann über visudo folgendes ergänzen:

# User hendrik can use apps needed for PostTLS
hendrik ALL = NOPASSWD: /usr/bin/mailq
hendrik ALL = NOPASSWD: /usr/sbin/postcat
hendrik ALL = NOPASSWD: /usr/sbin/postsuper

Also: Als Benutzer zunächst das Repository klonen:

$ mkdir ~/apps
$ cd ~/apps
$ git clone https://github.com/suenkler/PostTLS.git

Folgende Pakete installieren:

$ apt-get install python3-virtualenv python3-pip
$ apt-get install virtualenvwrapper

Nun eine virtuelle Python-Umgebung erstellen:

$ mkvirtualenv --python=/usr/bin/python3 posttls

Und die Abhängigkeiten installieren:

(posttls) $ pip install -r requirements.txt

Nun eine Datei env.sh erstellen und die Einträge entsprechend der eigenen Umgebung anpassen:

# Django configuration
export POSTTLS_SECRET_KEY="verysecretkey"
export POSTTLS_STATIC_ROOT_DIR="/home/hendrik/apps/posttls/static/"
export POSTTLS_MEDIA_ROOT_DIR="/home/hendrik/apps/posttls/media/"

# Set this to 'production' in production environment (see Django settings file)
export POSTTLS_ENVIRONMENT_TYPE="development"

# PostTLS settings
export POSTTLS_NOTIFICATION_SENDER="postmaster@domain.com (Postmaster)"
export POSTTLS_NOTIFICATION_SMTP_HOST="localhost"

# Needed to generate the links in the notification mail:
export POSTTLS_TLS_HOST="server.domain.com"
export POSTTLS_NOTIFICATION_SYSADMIN_MAIL_ADDRESS="sysadmin@domain.de"

Es sollte sicher gestellt sein, dass die oben genannten Verzeichnisse bestehen!

(posttls) $ source env.sh
(posttls) $ ./manage.py migrate

Nun sollte der folgende Befehl ohne Fehlermeldungen ausgeführt werden können:

(posttls) $ ./manage.py process_queue

Nun ist es an der Zeit, dass wir testen, ob PostTLS auch tut, was es soll. Dafür benötigen wir eine Mail, die wegen fehlender TLS-Unterstützung der Gegenseite in der Queue hängen bleibt. Das sieht dann so ähnlich aus:

$ mailq
-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
5D7FB1FC64    28198 Tue Mar  8 12:41:59  user@internal-domain.de
 (TLS is required, but was not offered by host mail.domain.de[192.168.10.7])
                                         user@external-domain.de

-- 28 Kbytes in 1 Request.

Wenn man nun erneut ./manage.py process_queue aufruft, sollte PostTLS dem Absender, hier user@internal-domain.de eine entsprechende Nachricht zusenden.

Der Entwicklungsserver von Django kann mit folgendem Befehl gestartet werden:

(posttls) $ ./manage.py runserver 0.0.0.0:8080

Bitte beachten Sie, dass die Links in der Benachrichtigungsmail auf „https“ lauten, der Entwicklungsserver aber nur die unverschlüsselte Variante unterstützt. Ändern Sie für diesen Test einfach die URL entsprechend.

Wenn alles funktioniert, kann es weiter gehen.

Produktiven Webserver einrichten

Bisher haben wir den Entwicklungsserver von Django verwendet. Dieser sollte im Produktivbetrieb natürlich nicht zum Einsatz kommen. Ich verwende immer gerne Gunicorn hinter einem Nginx Reverse Proxy.

Gunicorn

(posttls) $ pip install gunicorn

Das folgende Script liegt in /home/hendrik/apps/posttls/gunicorn_start:

#!/bin/bash
NAME="posttls"
DJANGODIR=/home/hendrik/apps/posttls/PostTLS/posttls
SOCKFILE=/home/hendrik/apps/posttls/run/gunicorn.sock 
USER=hendrik
GROUP=hendrik
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=config.settings.base
DJANGO_WSGI_MODULE=config.wsgi

echo "Starting $NAME"

cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH

# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR

# set environment variables
source /home/hendrik/apps/posttls/env.sh

# start application
exec /home/hendrik/.virtualenvs/posttls/bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
  --name $NAME \
  --workers $NUM_WORKERS \
  --user=$USER --group=$GROUP \
  --log-level=debug \
  --bind=unix:$SOCKFILE

Debian Jessie verwendet systemd. Wir erstellen also die Datei /etc/systemd/system/django.service mit folgendem Inhalt:

[Unit]
Description=PostTLS
After=network.target

[Service]
User=root
Group=root
WorkingDirectory=/home/hendrik/apps/posttls/PostTLS
ExecStart=/home/hendrik/apps/posttls/gunicorn_start

[Install]
WantedBy=multi-user.target

Anschließend können wir den neuen Service aktivieren und starten:

$ systemctl enable django.service
$ systemctl start django.service

Nginx

Nun fehlt noch Nginx. Zuerst wird Nginx installiert:

$ apt-get install nginx

Dann ergänzen wir folgenden Eintrag...

upstream posttls_server {
  server unix:/home/hendrik/apps/posttls/run/gunicorn.sock fail_timeout=0;
}

server {
    listen 8080 ssl;
    server_name posttls.domain.de;

    ssl_certificate     /etc/letsencrypt/live/posttls.domain.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/posttls.domain.de/privkey.pem;

    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/dhparams.pem;

    client_max_body_size 4G;

    access_log /home/hendrik/apps/posttls/logs/nginx-access.log;
    error_log /home/hendrik/apps/posttls/logs/nginx-error.log;

    location /static/ {
        alias   /home/hendrik/apps/posttls/static/;
    }

    location /media/ {
        alias   /home/hendrik/apps/posttls/media/;
    }

    location / {
        # an HTTP header important enough to have its own Wikipedia entry:
        #   http://en.wikipedia.org/wiki/X-Forwarded-For
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # enable this if and only if you use HTTPS, this helps Rack
        # set the proper protocol for doing redirects:
        proxy_set_header X-Forwarded-Proto https;

        # pass the Host: header from the client right along so redirects
        # can be set properly
        proxy_set_header Host $http_host;

        # we don't want nginx trying to do something clever with
        # redirects, we set the Host: header above already.
        proxy_redirect off;

        # Deny all except my ip adress
        #allow   XXX.XXX.XXX.XXX;
        #deny    all;

        # Serve static files from nginx
        if (!-f $request_filename) {
            proxy_pass http://posttls_server;
            break;
        }
    }
}

Falls Nginx aus dem Internet erreichbar sein muss, macht es ggf. Sinn, den Zugriff per IP-Adresse zu beschränken (oben auskommentiert):

allow   XXX.XXX.XXX.XXX;
deny    all;

Der Django-Entwicklungsserver liefert die Static Files direkt mit aus. Bei der Verwendung von Nginx müssen wir die Static Files aber noch in einem gesonderten Verzeichnis für Nginx verfügbar machen; vgl. dazu die Einstellungen von Nginx oben und die Django Settings. Sind diese Werte korrekt gesetzt, kann mit folgendem Befehl der Kopiervorgang gestartet werden:

(posttls) $ ./manage.py collectstatic

Dies sollte es nun sein. Wir testen das Ganze nochmals, indem wir eine Mail an einen Empfänger senden, dessen Mailserver kein TLS unterstützt:

  • Bleibt die Mail in der Queue hängen? => mailq
  • Aufruf von ./manage.py process_queue.
  • Kommt die Benachrichtigungsmail beim Absender an?
  • Funktionieren die Links in der Benachrichtigungsmail? Ist also unsere Nginx-Konfiguration korrekt?
  • Wird die Mail beim Klicken auf „unverschlüsselt versenden“ korrekt an den Empfänger zugestellt?

Wenn alles funktioniert, können wir den Vorgang automatisieren.

Automatisierung

Zu guter Letzt können wir einen Cron Job einrichten, der PostTLS jede Minute aufruft und damit in der Postfix Queue nach „hängengebliebenen“ Mails sucht und bei Erfolg die Absender per Mail darüber benachrichtigt. Wir verwenden dabei Flock, um sicher zu stellen, dass sich keine PostTLS-Prozesse überschneiden.

*/1 * * * * . /home/hendrik/apps/posttls/env.sh && /usr/bin/flock -w 0 /home/hendrik/apps/posttls/cron.lock /home/hendrik/.virtualenvs/posttls/bin/python3 /home/hendrik/apps/posttls/posttls/posttls/manage.py process_queue >/dev/null 2>&1

Ausblick

Es sind derzeit zugegebenermaßen eine Reihe von Schritten erforderlich, um PostTLS auf einem Server zu installieren. Soweit sinnvoll, werde ich diese Schritte in Zukunft automatisieren.

PostTLS setzt in der Regel jedoch an einer bestehenden, möglicherweise sehr komplexen Postfix-Installation an. Es wäre risikoreich und auch schlechter Stil, in diese bestehende Konfiguration automatisiert einzugreifen. Insofern werde ich mit der Automatisierung zurückhaltend sein.