Postfix E-Mail Signierung via GPG
-
System: Debian 13, ISPConfig, Nginx
-
Editor:
micro -
Letzte Änderung: 2026-07-02
1. GPG Schlüssel-Infrastruktur & Rechte
Aus Sicherheitsgründen darf der Postfix-eigene Nutzer (postfix) keine pipe-Dienste mit eigenen Privilegien starten. Der Signatur-Prozess läuft daher unter dem unprivilegierten Systembenutzer nobody:nogroup. GPG setzt zwingend voraus, dass das Home-Verzeichnis dem ausführenden Nutzer gehört und restriktive Rechte besitzt.
Verzeichnisstruktur anlegen & absichern
# 1. Neues GPG-Home-Verzeichnis erstellen
sudo mkdir -p /var/lib/gpg-signer
# 2. GPG-Schlüsselstruktur dorthin kopieren (oder neu importieren)
sudo cp -r /var/lib/postfix/.gnupg /var/lib/gpg-signer/
# 3. Besitzer auf 'nobody' ändern (Debian-Gruppe: nogroup)
sudo chown -R nobody:nogroup /var/lib/gpg-signer/.gnupg
# 4. Strikt erforderliche GPG-Rechte setzen
sudo chmod 700 /var/lib/gpg-signer/.gnupg
sudo chmod 400 /var/lib/gpg-signer/.gnupg/.gpg_passphrase
[!warning] Wichtig für GPG Sollten die Rechte nicht exakt auf
700(Ordner) und400(Passphrase) stehen, verweigert GPG die Entschlüsselung des privaten Schlüssels im Batch-Modus mit einem Permission Denied Fehler.
2. Postfix Konfiguration (/etc/postfix/master.cf)
Am Ende der /etc/postfix/master.cf wird der pipe-Dienst definiert. Zudem wird ein loopback-Port auf 10025 geöffnet, damit das Skript die signierte Mail ohne Filter-Endlosschleife wieder an Postfix übergeben kann.
# ==========================================================================
# GPG Mail Signer Service
# ==========================================================================
gpg-signer unix - n n - - pipe
flags=Rq user=nobody:nogroup argv=/usr/local/bin/gpg-sign.py ${sender} ${recipient}
127.0.0.1:10025 inet n - n - - smtpd
-o content_filter=
-o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
3. Python-Signatur-Skript (/usr/local/bin/gpg-sign.py)
Das Skript trennt die eingehende E-Mail in Header und Body, um zu verhindern, dass die Mail-Metadaten (wie Subject, To, From) mit-signiert werden, was den Mail-Header für Mailclients unlesbar machen würde. Danach wird die Mail via SMTP über Port 10025 re-injiziert.
#!/usr/bin/env python3
import os
import smtplib
import subprocess
import sys
GPG_BIN = "/usr/bin/gpg"
HOMEDIR = "/var/lib/gpg-signer/.gnupg"
KEY_ID = "D464EDA3A58C47C4"
PASS_FILE = "/var/lib/gpg-signer/.gnupg/.gpg_passphrase"
def sign_and_reinject():
if len(sys.argv) < 3:
sys.stderr.write("Error: Missing sender or recipient\n")
sys.exit(1)
sender = sys.argv[1]
recipient = sys.argv[2]
# Read full raw email from Postfix
raw_email = sys.stdin.buffer.read()
# Separate headers and body to only sign the body content
if b"\r\n\r\n" in raw_email:
headers, body = raw_email.split(b"\r\n\r\n", 1)
elif b"\n\n" in raw_email:
headers, body = raw_email.split(b"\n\n", 1)
else:
headers = b""
body = raw_email
env = os.environ.copy()
env["GPG_TTY"] = "/dev/null"
cmd = [
GPG_BIN,
"--homedir",
HOMEDIR,
"--batch",
"--yes",
"--no-tty",
"--pinentry-mode",
"loopback",
"--passphrase-file",
PASS_FILE,
"--clearsign",
"--local-user",
KEY_ID,
]
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
signed_body, err = proc.communicate(input=body)
if proc.returncode != 0:
sys.stderr.write(f"GPG Error: {err.decode()}\n")
sys.exit(1)
# Normalize line endings to strict Unix LF first for processing
headers_lf = headers.replace(b"\r\n", b"\n")
# Ensure UTF-8 MIME headers if missing (crucial for German Umlauts)
if b"MIME-Version:" not in headers_lf:
headers_lf += b"\nMIME-Version: 1.0"
if b"Content-Type:" not in headers_lf:
headers_lf += b"\nContent-Type: text/plain; charset=utf-8"
if b"Content-Transfer-Encoding:" not in headers_lf:
headers_lf += b"\nContent-Transfer-Encoding: 8bit"
# Convert everything to strict RFC-compliant CRLF (\r\n) for SMTP transmission
clean_headers = headers_lf.replace(b"\n", b"\r\n")
clean_body = signed_body.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
signed_email = clean_headers + b"\r\n\r\n" + clean_body
# Re-inject the email back into Postfix via the loop-free port
try:
with smtplib.SMTP("127.0.0.1", 10025) as server:
server.sendmail(sender, recipient, signed_email)
except Exception as smtp_err:
sys.stderr.write(f"SMTP Error: {smtp_err}\n")
sys.exit(1)
if __name__ == "__main__":
sign_and_reinject()
Rechte des Skripts anpassen:
Das Skript muss für den Benutzer nobody ausführbar sein:
sudo chown nobody:nogroup /usr/local/bin/gpg-sign.py
sudo chmod 755 /usr/local/bin/gpg-sign.py
4. Nützliche Befehle für den Betrieb
# Postfix Konfiguration nach Änderungen neu laden
sudo systemctl restart postfix
# E-Mail-Warteschlange (Queue) einsehen
mailq
# Postfix Queue sofort zwangsweise leeren (Zustellung erzwingen)
sudo postqueue -f
# Live-Logs der E-Mail-Zustellung überwachen
sudo tail -f /var/log/mail.log