Mailsystem Changes, Teil 1: rspamd

Manchmal passiert es in der IT, dass man über viele Jahre lang ein recht komplexes Setup betreibt, ohne das geringste Problem zu haben. Meist ist das ein Hinweis darauf, dass man entweder selten etwas an den komplexen Teilen ändern musste oder dass die Lösung zwar komplex war, die Problemdomäne aber dafür vollständig abgedeckt hat.

Die Komplexität bei mir lag im Mailsystem: Zusammengesteckt aus den Teilen Postfix, amavisd-new und SpamAssassin war das ganze eigentlich schon kompliziert genug, dazu kam aber noch die Idee, dass man die Fähigkeiten von amavisd-new als Framework nutzt und via Frontend den Benutzern die Möglichkeit gibt, sich ihre eigenen Spam- und Viren-Policies zu klicken.

Um es kurz zu machen: Das hat nie irgendwer benutzt. Mir bleibt also ein Datenbankschema, dass ich eigentlich auf Benutzername und Passwort herunterbrechen könnte, sowie Views und Funktionen, die die vordefinierten Policy-Typen, die ich eigentlich anbieten wollte, in amavisd-new-Konfigurationen übersetzen. Und dazu kam, dass der Betrieb von amavisd-new in einem pre-queue-Setup einfach sehr komplex werden kann.

Also habe ich heute angefangen, es abzureißen. Da ich die Framework-Fähigkeiten eh nicht gebraucht habe, und mir Erkennungsrate von SpamAssassin eh etwas zu gering war dachte ich mir, ich probiere etwas ganz neues und habe mich an rspamd gewagt.

Die Kommunikation zwischen Postfix und rspamd erfolgt über das Milter-Protokoll, was dann erstmal dazu geführt hat, dass ich nicht nur die ganze amavisd-new-Konfiguration losgeworden bin, sondern auch sämtliche Re-Injection-Listener für Postfix (wir erinnern uns: je nachdem, wo eine Mail hergekommen ist, waren unterschiedliche Optionen notwendig um nach der Durchlauf durch amavisd-new sicher zu stellen, dass die Empfänger richtig und zudem nicht doppelt behandelt wurden). Das war auf jeden Fall schon mal eine sehr angenehme Überraschung - auch, wenn ich mir abgewöhnen musste, smtp_generic_maps am zentralen Gateway zu benutzen, weil damit dann natürlich die DKIM-Signaturen kaputt gegangen sind.

Und damit sind wir schon bei der für mich eigentlich wichtigen Sache: rspamd kann irgendwie 99% der Features, die ich brauche, umsetzen, und alles was ich tun muss sind, drei oder vier winzige Konfigurationen fallen zu lassen:

root@mail:/etc/rspamd/local.d# ls -l
total 40
-rw-r----- 1 root _rspamd 109 Sep 11 13:39 antivirus.conf
-rw-r----- 1 root _rspamd 177 Sep 11 14:59 dkim_signing.conf
-rw-r----- 1 root _rspamd 296 Sep 11 13:53 dmarc.conf
-rw-r----- 1 root _rspamd 149 Sep 11 18:22 milter_headers.conf
-rw-r----- 1 root _rspamd 263 Sep 11 11:43 options.inc
-rw-r----- 1 root _rspamd  62 Sep 11 11:08 redis.conf
-rw-r----- 1 root _rspamd  98 Sep 11 14:56 spamassassin.conf
-rw-r----- 1 root _rspamd 903 Sep 11 11:25 statistic.conf
-rw-r----- 1 root _rspamd  60 Sep 11 10:49 worker-controller.inc
-rw-r----- 1 root _rspamd 229 Sep 11 10:49 worker-proxy.inc

Und wenn wir uns das der Reihe nach mal ansehen:

# antivirus.conf
clamav {
  attachments_only = false;
  servers = "/var/run/clamav/clamd.ctl";
}
# dkim_signing.conf
allow_hdrfrom_mismatch  = true;
allow_username_mismatch = true;
path                    = "/path/to/dkim.key";
selector                = "2017";
# dmarc.conf
send_reports = true;
report_settings {
  org_name           = "incertum.net";
  domain             = "incertum.net";
  email              = "posthamster@incertum.net";
  helo               = "localhost.localdomain";
  additional_address = "posthamster@incertum.net";
}
# milter_headers.conf
authenticated_headers = ["authentication-results", "x-spam-status"];
use = ["authentication-results", "x-spam-status"];
# options.inc
# managed by Class['rspamd']
local_addrs = "192.168.0.0/16, ..."
# redis.conf
servers = "redis-host";
# spamassassin.conf
ruleset     = "/var/lib/rspamd/spamassassin.cf";
match_limit = 100k;
# statistic.conf
classifier "bayes" {
    tokenizer {
        name = "osb";
    }

    backend    = "redis";
    min_tokens = 11;
    min_learns = 200;
    autolearn  = true;
    per_user   = true;
    statfile {
        symbol = "BAYES_HAM";
        spam = false;
    }
    statfile {
        symbol = "BAYES_SPAM";
        spam = true;
    }
    learn_condition =<<EOD
return function(task, is_spam, is_unlearn)
    local prob = task:get_mempool():get_variable('bayes_prob', 'double')

    if prob then
        local in_class = false
        local cl
        if is_spam then
            cl = 'spam'
            in_class = prob >= 0.95
        else
            cl = 'ham'
            in_class = prob <= 0.05
        end

        if in_class then
            return false,string.format('already in class %s; probability %.2f%%',
            cl, math.abs((prob - 0.5) * 200.0))
        end
    end

    return true
end
EOD
}
# worker-controller.inc
password = "verysecret";
# worker-proxy.inc
milter  = yes;  # Enable milter mode
timeout = 120s; # Needed for Milter usually
upstream "local" {
  default   = yes; # Self-scan upstreams are always default
  self_scan = yes; # Enable self-scan
}

Dabei ist der Inhalt der Datei statistic.conf zwar lang, aber 1:1 aus der Dokumentation kopiert - hier wird halt nur Redis verwendet. Letzteres hat mich kurz zurückschrecken lassen, aber es stellt sich raus, dass auch die redis.conf nur wenige Zeilen lang ist - und sogar zukünftige Replikation nicht ausschließt.

Ein bißchen Nacharbeit wr natürlich noch fällig:

  • der Job, der aus den Postfächern der User lernt, muss natürlich auf rspamc umgestellt werden
  • ebenso verwende ich die SpamAssassin-Regelwerke noch, ergo muss sich Puppet darum kümmern, dass der Update-Job aktiviert ist und in /etc/spamassassin/sa-update-hooks.d/ ein Hook angelegt wird, der die Regeln konkateniert
  • das Skript, welches die Logfiles auswertet (und dafür einfach logwatch benutzt) habe ich deutlich vereinfachen können, kein Grund mehr, nach Instanzen zu filtern
  • die nginx-Konfiguration brauchte noch einen Reverse-Proxy für den Zugriff auf das rspamd-Webinterface

Trotz allem hat die Umstellung nicht einmal ganz drei Stunden gedauert, und bisher bin ich wirklich beeindruckt.

Jetzt liegt noch eine Vereinfachung der Datenbank-Schemata vor mir, aber das wird noch warten müssen - ich berichte dann, wenn ich durch bin.