Mailsystem Changes 3: Dovecot, LMTP und Adress-Verifizierung

Wie ich ja schon schrieb hatte ich in den vergangenen zwei Wochen einige Änderungen an meinem privaten Mailsystem vorgenommen - allen voran den Umstieg von amavisd-new zu rspamd mit den dazugehörigen Vereinfachungen.

Ein Teil der noch blieb war, dass die Datenbank dahinter viel zu komplex war, weil ich ja geplant hatte, das alles irgendwann einmal via Weboberfläche konfigurierbar zu machen. Nachdem das nichts wurde und ich die Möglichkeiten eigentlich nie auch nur ansatzweise genutzt habe, habe ich das DB-Schema auf drei Werte (und eine ID natürlich) eingestampft: Username, Email-Adresse, Quota und Passwort.

Damit war das Datenbank-Schema deutlich einfacher, was ich allerdings schon immer als sehr unschön empfunden habe war, dass ich Postfix direkt Zugriff auf die PostgreSQL-Datenbank gewähren musste. Und eigentlich ist das nicht nötig. Ja, man benötigt keine weitere Konfiguration, aber dafür muss man die pgsql-Maps irgendwie raus rendern (mit Passwörtern drin), und das Schema dahinter wollte ich also in der Komplexität nicht mehr pflegen.

Also habe ich mich dazu entschieden, in Postfix nur noch eine Liste mit virtual_mailbox_domains zu pflegen und die vorhandenen Empfänger via Address Verification zu ermitteln. Die Liste der Domains pflege ich dabei in einem Text-File, welches ich später als Hash-Map einbinde:

root@mail:~# postconf virtual_mailbox_domains
virtual_mailbox_domains = hash:${maps_dir}/virtual_mailbox_domains
root@mail:~# cat ${maps_dir}/virtual_mailbox_domains
# managed by Class['postfix']
# NB: this is also used to selectively verify senders and
# recipients (transformed by makefile)
incertum.net   OK
billigmail.org OK

Für diese Domains möchte ich die Verifikation von Sendern und Empfängern aktivieren, aber dafür braucht man zusätzliche Steuer-Maps. Nachdem ich nun sowieso alle Postfix-Maps via make generiere war das relativ einfach:

# managed by Class['postfix']
all: [...] virtual_mailbox_domains.db verify_recipients.db verify_senders.db

%.db:	%
	postmap hash:$<

verify_recipients:
	sed 's/OK/reject_unverified_recipient/' virtual_mailbox_domains > $@

verify_senders:
	sed 's/OK/reject_unverified_sender/'    virtual_mailbox_domains > $@

(NB: Das ist so nicht 100%ig sicher vor race conditions: Wenn die Maps gerade neu aufgebaut werden während Postfix sie braucht, dann fliegt eventuell ein Fehler).

Das Einbinden ist dann relativ einfach:

address_verify_map = proxy:btree:${data_directory}/verify_cache
general_smtpd_restrictions =
    [...],
    check_recipient_access hash:${maps_dir}/verify_recipients,
    check_sender_access    hash:${maps_dir}/verify_senders
smtpd_recipient_restrictions =
    [...],
    $general_smtpd_restrictions

Den Parameter address_verify_map setze ich hier übrigens einfach nur, um eine irreführende, harmlose aber nervige Fehlermeldung loszuwerden - das funktioniert mit hinreichend modernem Postfix auch so, siehe die oben verlinkte Dokumentation dazu.

Wie findet nun die Verifikation statt? Via LMTP, und somit muss man Postfix auf beibringen, dass es Dovecot via LMTP kontaktieren soll:

virtual_transport = lmtp:unix:private/dovecot-lmtp

Das korrespondiert mit der Konfiguration des lmtp-Listeners in der Dovecot-Konfiguration:

service lmtp {
  user = vmail

  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    user  = postfix
    group = postfix
    mode  = 0660
  }
}

Wie man sieht gehe ich davon aus, dass der lmtp(8)-Client von Postfix in einer chroot-Umgebung läuft (nämlich unterhalb von /var/spool/postfix, welches auch das $queue_directory ist).

Alles was noch übrig ist war, Dovecot zu sagen, wo seine Nutzerinformationen herkommen. Hier gibt’s eine kleine Besonderheit: Meine Puppet-Module sind so eingestellt, dass bestimmte Hosts einen eigenen SASL-User bekommen, damit sie Mails verschicken können (realisiert via Exported Resources). Diese User sind in einer einfachen Text-Datenbank gespeichert. Glücklicherweise bietet Dovecot die Möglichkeit, mehr als eine Password Database zu verwalten. Konkret sieht das ganze dann so aus:

# plain text password backend
passdb {
  args           = scheme=PLAIN username_format=%n /etc/dovecot/sasl_users
  default_fields = userdb_quota_rule=*:storage=1M
  driver         = passwd-file
}
# SQL password backend
passdb {
  args           = /etc/dovecot/dovecot-sql.conf.ext
  default_fields = userdb_quota_rule=*:storage=1M
  driver         = sql
}
# static user detail backend
userdb {
  args   = uid=X000 gid=X000 home=/path/to/mail/store/%n
  driver = static
}

Wie man sieht ist neben dem Passwort die einzige pro User veränderliche Größe das Quota :-)

Da wir eine statische User Database verwenden, muss das SQL-Query natürlich alle Variablen Informationen liefern. Dementsprechend sieht das dann in /etc/dovecot/dovecot-sql.conf.ext folgendermaßen aus:

password_query = \
  SELECT password, quota_rule userdb_quota_rule FROM user_data \
  WHERE (username = '%u' OR primary_address = '%u') AND active = true

Wenn der Username nicht die Email-Adresse ist muss man hier, wie gezeigt, mit einem OR arbeiten.

Tja, und mehr ist es nicht, das ganze funktioniert dann auf Anhieb:

Sep 20 04:55:08 mail postfix/smtpd[29580]: connect from astarte.incertum.net[46.38.238.31]
Sep 20 04:55:09 mail postfix/smtpd[29580]: Anonymous TLS connection established from astarte.incertum.net[46.38.238.31]: TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (
256/256 bits)
Sep 20 04:55:09 mail postfix/cleanup[29384]: 082E5585: message-id=<20170920025509.082E5585@mail.incertum.net>
Sep 20 04:55:09 mail postfix/qmgr[17409]: 082E5585: from=<double-bounce@mail.incertum.net>, size=238, nrcpt=1 (queue active)
Sep 20 04:55:09 mail dovecot: lmtp(29585): Connect from local
Sep 20 04:55:09 mail dovecot: lmtp(29585): Disconnect from local: Successful quit
Sep 20 04:55:09 mail postfix/lmtp[29389]: 082E5585: to=<cite+astarte-root@incertum.net>, relay=mail.incertum.net[private/dovecot-lmtp], delay=0.09, delays=0.07/0/0.01/0.0
1, dsn=2.1.5, status=deliverable (250 2.1.5 OK)
Sep 20 04:55:09 mail postfix/qmgr[17409]: 082E5585: removed
Sep 20 04:55:12 mail postfix/smtpd[29580]: 1C3C3585: client=astarte.incertum.net[46.38.238.31]
Sep 20 04:55:12 mail postfix/cleanup[29384]: 1C3C3585: message-id=<20170920025508.1660C9E2E0@astarte.incertum.net>
Sep 20 04:55:12 mail postfix/qmgr[17409]: 1C3C3585: from=<cite+astarte-root@incertum.net>, size=1146, nrcpt=1 (queue active)
Sep 20 04:55:12 mail dovecot: lmtp(29585): Connect from local
Sep 20 04:55:12 mail postfix/smtpd[29580]: disconnect from astarte.incertum.net[46.38.238.31] ehlo=2 starttls=1 mail=1 rcpt=1 data=1 quit=1 commands=7
Sep 20 04:55:12 mail dovecot: lmtp(cite@incertum.net): IWPJEpDYwVmRcwAAH6dZUA: sieve: msgid=<20170920025508.1660C9E2E0@astarte.incertum.net>: stored mail into mailbox 'IN
BOX'
Sep 20 04:55:12 mail postfix/lmtp[29389]: 1C3C3585: to=<cite@incertum.net>, relay=mail.incertum.net[private/dovecot-lmtp], delay=3.5, delays=3.3/0/0/0.22, dsn=2.0.0, stat
us=sent (250 2.0.0 <cite@incertum.net> IWPJEpDYwVmRcwAAH6dZUA Saved)
Sep 20 04:55:12 mail dovecot: lmtp(29585): Disconnect from local: Successful quit
Sep 20 04:55:12 mail postfix/qmgr[17409]: 1C3C3585: removed

(NB: Hier sieht man übrigens auch, wie ich Adressen umschreibe: da root@astarte.incertum.net im Fall dess Falles nicht zustellbar ist, schreibe ich mittels RegExp-Tabelle in smtp_generic_maps den Absender um zu cite+astarte-root@incertum.net.)

Wir sehen hier, dass der smtpd(8) mit der PID 29580 eine Verbindung annimmt. Sobald der Empfänger übermittelt generiert Postfix eine Address Verification Probe mit Absender double-bounce@mail.incertum.net (Queue-ID 082E5585) und stellt fest, dass man an die genannte Adresse zustellen kann. Die neue Mail erhält die Queue-ID 1C3C3585 und wird erfolgreich via LMTP an Dovecot übermittelt.

Die drei Sekunden Verzögerung sind wahrscheinlich der rspamd-Milter. Falls das immer so lange dauern sollte werde ich mir das noch einmal ansehen und dann hier berichten :-)