S/MIME-Verschlüsselung durch Postfix-Filter

S/MIME ist ein Standard, mit dem Emails verschlüsselt und signiert werden können (http://de.wikipedia.org/wiki/S/MIME). Zwar bieten die meisten Email-Clients entsprechenden Support - manchmal ist es aber sinnvoll, den Teil der Verschlüsselung auf eine zentrale Komponente, z.B. ein vorhandenes Mailrelay, auszulagern. So ist sichergestellt, das vertrauliche Emails das Unternehmensnetz nur verschlüsselt verlassen, ohne dass der Benutzer beim Erstellen der Email mit der Verschlüsselung in Berührung kommt.

Für diese Aufgabe gibt es sicherlich eine lange Liste von fertigen Appliances - mit etwas Bash-Scripting, openssl und der Definition eines Content-Filters lässt sich diese Aufgabe jedoch ohne allzu großen Aufwand auf einem Postfix-Mailrelay selber implementieren.



Die Aufgabe:

Verschlüssel von Emails an ausgewählte Empfänger direkt auf dem Ausgangs-MTA. Die Verschlüsselung soll mit S/MIME erfolgen.

Zutaten:

  • Benutzerzertifikate für die Empfänger der verschlüsselten Emails
  • openssl für die S/MIME-Verschlüsselung
  • reformail (oder formail) um effektiv Header aus Emails zu extrahieren (wie sich zeigen wird, muss eigentlich nur der "Subject:"-Header extrahiert werden, was sicherlich auch mit einem einfachen grep schnell erledigt wäre)
  • postfix 

Benutzerzertifikate


Von den Benutzern, denen wir S/MIME-verschlüsselte Emails senden wollen, benötigen wir das Benutzerzertifikat. Wenn der Benutzer ein Domänen-Benutzer unter Windows ist, lässt sich das Zertifikat sehr einfach über das MMC-SnapIn "Zertifikate" exportieren (siehe z.B. http://technet.microsoft.com/de-de/library/cc730988.aspx). Für das Beispiel hier habe ich das Zertifikat base64-codiert exportiert.

S/MIME-Verschlüsselung mit openssl


Das Werkzeug "openssl" stellt ein Unterkommando "smime" zur Verfügung, mit dem alle wichtigen S/MIME Aktionen durchgeführtr werden können (verschlüsseln, entschlüsseln, signieren, Signatur überprüfen). Die zu behandelnde Email (Header+Body) wird dabei entweder per Standardeingabe oder in einer Datei zur Verfügung gestellt.

Die beim Verschlüsseln entstehende Email wird auf der Standardausgabe ausgegeben oder man gibt mit dem Parameter "-out" eine Ausgabedatei vor.

Beispiel

(Benutzerzertifikat in "zertifikat.cer", zu verschlüsselnde Email in "message.txt")

openssl smime -encrypt -des3 -out message.encrypted zertifikat.cer < message.txt

Im Anschluss daran enthält die neue Datei "message.encrypted" die verschlüsselte Email:

MIME-Version: 1.0
Content-Disposition: attachment; filename="smime.p7m"
Content-Type: application/x-pkcs7-mime; smime-type=enveloped-data; name="smime.p7m"
Content-Transfer-Encoding: base64

MIJSGAYJKoZIhvcNAQcDoIJSCTCCUgUCAQAxgfYwgfMCAQAwgZwwgY4xCzAJBgNV
BAYTAkRFMRAwDgYDVQQIEwdTYWNoc2VuMRAwDgYDVQQHEwdEcmVzZGVuMRwwGgYD
...

Die originale Email (incl. der Header) wurde verschlüsselt und als Anhang mit dem Namen "smime.p7m" in eine neue Email verpackt. 

Diese Email könnte jetzt direkt per sendmail an einen MTA übergeben und an den Zielbenutzer gesendet werden. Allerdings wird die Email noch "etwas schöner", wenn wir zumindest noch drei für den Empfänger interessante Header hinzufügen: "From:", "To:" und das "Subject:". 

Diese Arbeit nimmt uns openssl mit den zusätzlichen Parametern "-from" "-to" und "-subject" ab:

openssl smime -encrypt -des3 -out message.encrypted \
    -from absender@domain1.tld -to empfaenger@domain2.tld \
    -subject "Eine geheime Nachricht" zertifikat.cer < message.txt

Jetzt sieht der Header der entstandenen Email schon besser aus:

To: empfaenger@domain2.tld
From: absender@domain1.tld
Subject: Eine geheime Nachricht
MIME-Version: 1.0
Content-Disposition: attachment; filename="smime.p7m"
...

Beim Einsatz dieses Kommandos im Content-Filter werden für Absender, Empfänger und Betreff dann die Parameter der originalen Nachricht verwendet.

Content-Filter im Postfix


Postfix bietet verschiedene Möglichkeiten, externe Content-Filter zu implementieren. Eine relativ einfache Variante ist, das Postfix Kommando "pipe" zu verwenden, und damit ein beliebiges Skript mit den Daten der Email auf der Standardeingabe zu versehen.

Angenommen es existiert ein Skript "/usr/local/bin/mein_filter.sh", dass Emails (Header+Body) auf der Standardeingabe akzeptiert und dann - wie auch immer - weiter verarbeitet und zurück in das Mailsystem gibt. 

Um dieses Skript als Filter zu verwenden, muss zuerst in "/etc/postfix/master.cf" ein entsprechender Transport-Mechanismus definiert werden:

/etc/postfix/master.cf
...
meinfilter  unix  -       n       n       -       2       pipe
    flags=Rq user=filter null_sender= argv=/usr/local/bin/mein_filter.sh -f ${sender} -- ${recipient}
...

Dadurch existiert ein neuer Transport-Mechanismus mit dem Namen "meinfilter", der bei Verwendung unter dem Benutzeraccount "filter" das Skript "/usr/local/bin/mein_filter.sh" aufruft und mit der Email auf der Standardeingabe versorgt.

Die Parameter für das Skript ( "-f ${sender} -- ${recipient}") sind so gewählt, dass Sie später 1:1 an das Kommando sendmail übergeben werden können.

Zusätzlich wird in der Postfix-Konfiguration für den neuen Transportmechanismus festgelegt, dass er für jeden einzelnen Empfänger einer Email separat aufgerufen werden muss. Das kostet zwar Performance, spart aber etwas Intelligenz im Filter-Skript:

/etc/postfix/main.cf
...
# meinfilter benötigt alle recipients einzeln
meinfilter_destination_recipient_limit = 1
...

Das Filter-Skript


Das Filter-Skript soll die Email auf der Standard-Eingabe entgegennehmen, verarbeiten (also verschlüsseln) und wieder an den MTA übergeben. Im einfachsten Fall könnte so ein Filter wie folgt aussehen:

#!/bin/bash

# Die Standardeingabe (die Email) zwischenspeichern
cat > /tmp/message.$$

# Email veschlüsseln
openssl smime -encrypt -des3 \
    -out /tmp/message.$$.smime zertifikat.cer < /tmp/message.$$

# Email senden (die Parameter aus der master.cf werden 1:1 weitergegeben)
$SENDMAIL "$@" < message.$$.smime

# aufräumen
rm message.$$*

Der Filter ist so natürlich noch nicht einsatzfähig: Zum einen müssen wir ja für jeden Empfänger ein eigenes Zertifikat (nämlich dessen Zertifikat) verwenden - und außerdem fehlen der entstandenen Email noch die drei wichtigen Header von oben  - "From:", "To:" und "Subject:".

Den Subject-Header extrahieren wir am besten aus der originalen Email. Dafür ist das Kommando reformail gut geeignet (bei ubuntu im Paket  maildrop). Folgendes Kommando liefert den Betreff der Email:

        reformail -x "Subject:" < message.$$

Die "From:" und "To:" Header können aus Bequemlichkeit einfach aus den Kommandozeilenparameter an das Skript übergeben werden (SENDER="$2"; RECIPIENT="$4").

Die Zertifikate für die einzelnen Empfänger liegen im einfachsten Fall in einem gemeinsamen Verzeichnis und die Dateinamen entsprechen der Email-Adresse des Empfängers.

Und jetzt alles zusammen ....


Wenn wir das alles zusammennehmen, könnte ein Proof-of-Concept Filter-Skript zum Verschlüsseln ausgehender Emails so aussehen:

/usr/local/bin/mein_filter.sh
#!/bin/bash

WORKDIR="/tmp"
SENDMAIL="/usr/sbin/sendmail -G -i"
CERTS="/etc/mailcerts"

EX_UNAVAILABLE=69

SENDER="$2"; RECIPIENT="$4"

if test -f "$CERTS/$RECIPIENT.crt" ; then

        MESSAGEFILE="$WORKDIR/message.$$"
        trap "rm -f $MESSAGEFILE; rm -f $MESSAGEFILE.encrypted" 0 1 2 3 15
        umask 077

        cat > $MESSAGEFILE \
            || { echo Cannot save mail to file; exit $EX_UNAVAILABLE; }

        SUBJECT=$(reformail -x "Subject:" < $MESSAGEFILE)

        openssl smime -encrypt -des3 -out $MESSAGEFILE.encrypted\
                -from $SENDER -to $RECIPIENT \
                -subject "$SUBJECT" $CERTS/$RECIPIENT.crt < $MESSAGEFILE \
                || { echo Problem encrypting message; exit $EX_UNAVAILABLE; }

        $SENDMAIL "$@" < $MESSAGEFILE.encrypted
        exit $?
else
        cat | $SENDMAIL "$@"
        exit $?
fi

Falls ein Email-Empfänger mit der Adresse benutzername@domain.tld verschlüsselte Emails erhalten soll, muss dessen Benutzerzertifikat in das Verzeichnis "/etc/mailcerts" unter dem Dateinamen "benutzername@domain.tld.cert" abgelegt werden. Wird im Skript eine entsprechende Datei gefunden, wird die Email per openssl verschlüsselt. Ist kein Zertifikat hinterlegt, wird die Email ungefiltert an das sendmail-Kommando übergeben.

Verschlüsselung aktivieren


Nach ausreichenden Tests und Optimierungen am Skript (Fehlerbehandlung!) kann die endgültige Version des Filters global aktiviert werden. Dazu wird in "/etc/postfix/master.cf" der neu eingeführte Transport-Mechanismus als Content-Filter für den smtpd-Dienst aktiviert:

/etc/postfix/master.cf
...
smtp      inet  n       -       -       -       -       smtpd
        -o content_filter=meinfilter:dummy

meinfilter  unix  -       n       n       -       2       pipe
    flags=Rq user=filter null_sender= 
    argv=/usr/local/bin/mein_filter.sh -f ${sender} -- ${recipient}
...

Für genauere Infos siehe "man 5 master".

Fazit


Der vorgestellte Mechanismus ist sicherlich nicht für jeden Anwendungsfall geeignet. So werden z.B. _alle_ Emails (je nach Anwendungsfall also auch die eingehenden) durch den Filter geschickt. Die Implementierung als Shell-Skript könnte bei hoch ausgelasteten Systemen zu Performance-Problemen führen. Trotzdem kann der gezeigte Ansatz aus meiner Sicht (mit kleinen Optimierungen) durchaus an in Produktiv-Umgebungen eingesetzt werden. Eine Erweiterung des Konzepts auf das Signieren von Emails ist natürlich genauso denkbar.