Files
axion1337.chat-gitops/docs/deployment-guides/06-moderation-content-scanning.md
T

7.6 KiB

Moderation Bot & Content Scanning

Status: Draupnir deployed (2026-07-29, Closes Issue #18) | Content Scanner deployed + live getestet (2026-07-29, Closes Issue #19) Konfiguration: apps/production/draupnir*.yaml, apps/production/clamav*.yaml, apps/production/clamav_spam_checker.py

1. Draupnir (Moderationsbot)

Community-Nachfolger von Mjolnir. Läuft als eigener Bot-Account (@draupnir:axion1337.chat), verwaltet Ban-Listen ("Policy Rooms") und setzt sie in geschützten Räumen durch.

Warum Draupnir statt Mjolnir?

Mjolnir gilt als Vorgänger-Projekt und wird von der Community nicht mehr aktiv weiterentwickelt; Draupnir ist der aktive Fork mit denselben Kernfunktionen plus Erweiterungen (u.a. native Rust-Crypto-Unterstützung, siehe unten).

Bot-Account & Zugriff (Bootstrap)

Da Authentifizierung über MAS läuft (kein klassisches registration_shared_secret), wird der Bot-Account über MAS' eigenes CLI-Tool angelegt:

kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
  mas-cli manage register-user draupnir --yes

kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
  mas-cli manage issue-compatibility-token draupnir

Der ausgegebene Token wird per sops apps/production/draupnir-secret.yaml manuell eingetragen (kein automatisierter Schritt - der Token darf nirgends unverschlüsselt landen).

Wichtige Stolpersteine (live gefunden, nicht aus der Doku ableitbar)

  • Version: gnuxie/draupnir:v2.9.0 crasht beim ersten Start mit initialManager ("Can't join remote room because no servers..."). Das automatische Anlegen des Management-Rooms über initialManager funktioniert erst ab v3.1.0. Aktuell deployt: v3.1.0.
  • CLI-Argument statt Env-Var: v3.x hat die automatische Config-Erkennung über NODE_CONFIG_DIR entfernt - der Container braucht jetzt explizit args: ["bot", "--draupnir-config", "/data/config/default.yaml"], sonst TypeError: No configuration path has been found for Draupnir. (per Extraktion von dist/config.js aus dem Image bestätigt, nicht dokumentiert gefunden).
  • NetworkPolicy: Der Bot muss Synapse direkt anrufen können. Da matrix-stack-synapse intern über haproxy geroutet wird und allow-ingress-haproxy standardmäßig nur Traefik (kube-system) erlaubt, braucht Draupnir eine eigene podSelector-Ausnahme in networkpolicy.yaml - sonst schlägt jede Anfrage an den Homeserver silent fehl.

Verschlüsselter Management-Room

Standardmäßig unverschlüsselt (Draupnirs zugrundeliegende Bot-Library aktiviert Crypto nicht automatisch). Für einen verschlüsselten Management-Room:

  1. experimentalRustCrypto: true in der Config ergänzen (via sops) - vom Hersteller selbst als "not considered production safe" gekennzeichnet, in unserem Test aber ohne Fehler gelaufen (Pod stabil, kein Crash, End-to-end encryption enabled in den Logs).
  2. Verschlüsselung ist eine Raum-Eigenschaft, die beim Erstellen gesetzt wird - das Flag allein verschlüsselt einen bereits bestehenden Management-Room nicht rückwirkend. Dafür in Element: Raumeinstellungen → Sicherheit & Datenschutz → Verschlüsselung aktivieren.

Profilbild setzen

Erfordert eine mxc://-URL (Bild muss zuerst hochgeladen werden, z.B. per Chat an den Bot senden, dann in Element per "View Source" die mxc://-URL kopieren):

!draupnir avatar mxc://<server>/<media-id>

Befehle (Kurzreferenz)

Alle Befehle im (verschlüsselten) Management-Room, Präfix !draupnir:

Befehl Zweck
status Bot-Status, beobachtete Listen, geschützte Räume
rooms add <room> Raum unter Draupnirs Schutz stellen (Voraussetzung für Bans!)
list create <shortcode> <alias> Neue Policy-Liste anlegen (wird automatisch beobachtet + geschützt)
watch <shortcode> Zusätzliche Policy-Liste beobachten
ban <user> <liste> <grund> Wichtig: 2. Argument ist die Policy-Liste, NICHT der Ziel-Raum! Der Ban gilt automatisch in allen Räumen, die diese Liste beobachten und geschützt sind
kick <user> <room> <grund> Direkter, sofortiger Kick aus einem konkreten Raum (ohne Listen-Umweg)
rules Zeigt die Regeln einer Policy-Liste an
unban <user> <liste> Regel wieder entfernen

Live getestet (2026-07-29): Testraum geschützt, Policy-Liste angelegt, Testnutzer über ban+Liste erfolgreich aus dem geschützten Raum entfernt. Kernmechanismus bestätigt funktionsfähig.

2. Content Scanner (Issue #19)

Verworfener erster Ansatz: matrix-content-scanner-python ist ein Proxy, den der Client explizit statt der normalen Media-Endpunkte aufrufen muss - Synapse selbst leitet nichts automatisch dorthin um. Diese client-seitige Unterstützung existiert nur noch in veralteten, nicht mehr gepflegten Android/iOS-SDKs; weder aktuelles Element Web noch Element X unterstützen das (geprüft: kein content_scanner-Hook im offenen element-x-android-Repo). Element selbst hat echtes serverseitiges Scanning - aber nur in der kommerziellen Element Pro + ESS Pro-Kombination, nicht in unserer offenen ESS-Community-Installation.

Tatsächlich umgesetzt: ein eigenes, kleines Synapse-Modul (clamav_spam_checker.py), das Synapses echten, dokumentierten Hook check_media_file_for_spam nutzt - läuft serverseitig, transparent für jeden Client, ganz ohne Mitwirkung des Clients. Kein fertiges Modul dafür existiert (auch das verbreitete synapse-http-antispam-Brückenmodul schließt genau diesen Callback explizit aus), daher selbst geschrieben.

Architektur:

  • ClamAV (clamav/clamav:1.5.3) läuft als eigener Pod, PVC für die Signatur-Datenbank.
  • Das Modul (apps/production/clamav_spam_checker.py) wird per ConfigMap gemounted und über PYTHONPATH importierbar gemacht (synapse.extraVolumes/extraVolumeMounts/extraEnv - kein Custom-Synapse-Image nötig).
  • Spricht ClamAVs natives INSTREAM-Protokoll direkt über Twisted-Netzwerk-Primitives (HostnameEndpoint/connectProtocol), nicht über asyncio - Synapse läuft auf Twisteds Reactor, nicht auf einer laufenden asyncio-Event-Loop. Ein erster Versuch mit asyncio.open_connection/wait_for schlug live mit RuntimeError: no running event loop fehl und fiel dadurch (durch das eigene Fail-Open-Verhalten) unbemerkt auf "durchlassen" zurück - die EICAR-Testdatei wurde beim ersten Versuch nicht erkannt. Nach Umstellung auf Twisted-Primitives funktioniert es sauber.
  • Fail-open bei Scanner-Fehlern (Verbindungsfehler/Timeout → Datei wird durchgelassen, laut geloggt) - ein ClamAV-Ausfall soll nicht alle Uploads auf dem Homeserver blockieren.

Live getestet und bestätigt (2026-07-29):

  • Normale Datei in unverschlüsseltem Raum → läuft durch (kein Regressionsschaden).
  • EICAR-Testdatei in unverschlüsseltem Raum → zuverlässig blockiert (ClamAV rejected an upload: Eicar-Test-Signature, Client bekommt 400 Bad content - Synapse gibt bewusst keine Begründung an den Client zurück, nur in den Server-Logs sichtbar).
  • EICAR-Testdatei in verschlüsseltem Raum/DM → läuft durch - erwartete, strukturelle Grenze: Synapse hat bei E2EE nie den Entschlüsselungsschlüssel, sieht nur Ciphertext. Nur ein kooperierender Client könnte das lösen (siehe oben, existiert nicht offen verfügbar).

Bekannte Deckungslücke: schützt nur unverschlüsselte Räume/DMs (viele öffentliche/ föderierte Räume) - keine Warnung/Kennzeichnung für Nutzer in verschlüsselten Räumen, dass dort kein Scanning stattfindet. Folgeidee (Issue #43, LOW): Grafana-Dashboard über die bestehenden Loki-Logs, um Erkennungen/Scanner-Ausfälle sichtbar zu machen.