13 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.0crasht beim ersten Start mitinitialManager("Can't join remote room because no servers..."). Das automatische Anlegen des Management-Rooms überinitialManagerfunktioniert 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_DIRentfernt - der Container braucht jetzt explizitargs: ["bot", "--draupnir-config", "/data/config/default.yaml"], sonstTypeError: No configuration path has been found for Draupnir.(per Extraktion vondist/config.jsaus dem Image bestätigt, nicht dokumentiert gefunden). - NetworkPolicy: Der Bot muss Synapse direkt anrufen können. Da
matrix-stack-synapseintern über haproxy geroutet wird undallow-ingress-haproxystandardmäßig nur Traefik (kube-system) erlaubt, braucht Draupnir eine eigenepodSelector-Ausnahme innetworkpolicy.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:
experimentalRustCrypto: truein der Config ergänzen (viasops) - vom Hersteller selbst als "not considered production safe" gekennzeichnet, in unserem Test aber ohne Fehler gelaufen (Pod stabil, kein Crash,End-to-end encryption enabledin den Logs).- 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 überPYTHONPATHimportierbar gemacht (synapse.extraVolumes/extraVolumeMounts/extraEnv- kein Custom-Synapse-Image nötig). - Spricht ClamAVs natives INSTREAM-Protokoll direkt über Twisted-Netzwerk-Primitives
(
HostnameEndpoint/connectProtocol), nicht überasyncio- Synapse läuft auf Twisteds Reactor, nicht auf einer laufenden asyncio-Event-Loop. Ein erster Versuch mitasyncio.open_connection/wait_forschlug live mitRuntimeError: no running event loopfehl 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 bekommt400 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 (Stand vor der Client-Erweiterung unten): schützt nur unverschlüsselte Räume/DMs - 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.
3. Client-seitiges Scanning für verschlüsselte Räume (Issue #19-Erweiterung, 2026-07-29)
Da Synapse bei E2EE-Räumen strukturell nie den Schlüssel hat, kann nur der Client
Klartext scannen - einmal beim Senden (vor der Verschlüsselung), einmal beim Empfangen
(nach der Entschlüsselung). Umgesetzt in ThreadNet-Web (Fork von Element Web).
Architektur
Ein neuer, eigener HTTP-Dienst (apps/production/clamav-http-scanner.py, eigenes Image via
clamav-http-scanner-Dockerfile) macht denselben ClamAV-Pod für Browser-JS erreichbar
(clamd spricht nur rohes TCP, das kann ein Browser nicht). Erreichbar unter
https://axion1337.chat/_scan. Auth über Synapses eigenen
/_matrix/client/v3/account/whoami-Endpunkt (kein eigenes Auth-System nötig) - verhindert,
dass der Dienst zu einem offenen "teste dein Malware gegen unseren Virenscanner"-Orakel für
das ganze Internet wird. Fail-open bei Scanner-Fehlern, wie beim Synapse-Modul.
Zwei Patch-Stellen im ThreadNet-Web-Fork (im Repo
rohana.axion1337.de/sorb/ThreadNet-Web.git, nicht in diesem gitops-Repo):
- Empfang:
apps/web/src/utils/DecryptFile.ts, FunktiondecryptFile()- der einzige Punkt im ganzen Client, an dem entschlüsselte Klartext-Bytes für jeden Anhangstyp entstehen (Bild/Audio/Video/Datei laufen alle überMediaEventHelperhier durch). Scan direkt nach dem Entschlüsseln, vor der Rückgabe alsBlob. - Versand:
apps/web/src/ContentMessages.ts, FunktionuploadFile()- die eine gemeinsame Funktion für alle Anhangs-Uploads (Hauptdatei, generierte Thumbnails, Sprachnachrichten), unabhängig davon ob der Zielraum verschlüsselt ist. Scan direkt nach dem Einlesen der Datei, vor Verschlüsselung/Upload. - Gemeinsame Hilfsdatei:
apps/web/src/utils/ContentScanner.ts(neuescanContent()- Funktion +ContentScanRejectedError), von beiden Stellen genutzt. Fehlertexte über die bereits bestehenden Error-Rendering-Pfade inMImageBody.tsx/MAudioBody.tsx/VideoBodyViewModel.ts/FileBodyViewModel.ts(gleiches Muster wie die schon vorhandenenDecryptError/DownloadError).
Live getestet (2026-07-29):
- EICAR in verschlüsseltem Gruppenraum ("testgruppe") und in 1:1-DMs zwischen zwei echten Accounts - in beiden Fällen zuverlässig vor dem Upload blockiert. Vorher (nur Synapse-Modul) lief das durch.
- Empfangsseite unabhängig vom Absender bestätigt: EICAR über einen echten, ungepatchten
Client (app.element.io) in denselben verschlüsselten Raum geschickt (simuliert einen
fremden/föderierten Absender ohne unseren Patch) - beim Download-/Anzeigeversuch im
gepatchten
ThreadNet-Web-Client greift der Scanner zuverlässig. Beweist, dass der Empfangs-Hook unabhängig vom sendenden Client funktioniert, nicht nur als Selbstschutz für eigene Uploads.
⚠️ Wichtig für Desktop-/Electron-Builds (korrigiert, siehe Issue #44)
Dieser Fix ist im Web-Client (Browser, das laufende threadnet-web-Container-Image)
bestätigt live wirksam. Ob er auch im Electron-Client wirkt, hängt am tatsächlichen
Build-Prozess - und der ist aktuell nicht automatisiert.
Element Desktop (apps/desktop im selben Monorepo) baut die Web-App nicht selbst, sondern
packt ein fertiges webapp-Verzeichnis in ein webapp.asar. Woher dieses Verzeichnis
kommt, hängt vom Aufrufer ab:
- Standard-Fallback (
pnpm run fetch <version>ohne Artefakt): lädt ein offiziell vonelement-hq/element-websigniertes Release-Tarball herunter - Upstream, ohne unsere Patches. - Mit eigenem Build (
webapp-artifact-Mechanismus inbuild_desktop_prepare.yaml, gedacht für CI): würde unseren eigenenapps/web-Output übernehmen, inklusive aller Fork-Anpassungen.
Der zweite Weg ist im Repo als GitHub-Actions-Pipeline (build-and-test.yaml) angelegt,
läuft aber nicht automatisch - kein registrierter Runner, und der vorgelagerte Build-Job
checkt zudem noch element-hq/element-web (Upstream) statt des eigenen Forks aus, ein Rest
der ursprünglichen Upstream-CI. Die bereits existierende Desktop-Build (mit der
Discord-Style-Raumliste) entstand nach aktuellem Stand aus einem manuellen, lokalen
Build-Durchlauf, nicht aus einem reproduzierbaren, automatisierten Prozess.
Konsequenz für heute: die Scan-Patches sind im ThreadNet-Web-Fork-Code enthalten und
würden in jedem zukünftigen (manuellen oder automatisierten) Desktop-Build aus diesem Fork
mitkommen - sie sind aber nicht automatisch in einer bereits existierenden
Desktop-Installation gelandet, ohne dass jemand den Build-Vorgang erneut manuell durchführt.
Neues Backlog-Item dafür angelegt: Issue #44 - Build-Job auf den eigenen Fork umstellen + funktionierenden Runner aufsetzen, damit Fork-Änderungen zuverlässig und automatisch auch im Desktop-Client landen.
Element X (Mobile, iOS/Android) ist davon komplett unberührt - eigene Codebasis auf
Basis von matrix-rust-sdk, kein gemeinsamer Code mit ThreadNet-Web. Ein Schutz dort
wäre ein separates, eigenständiges Projekt.