# 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: ```bash 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:/// ``` ### 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 ` | Raum unter Draupnirs Schutz stellen (Voraussetzung für Bans!) | | `list create ` | Neue Policy-Liste anlegen (wird automatisch beobachtet + geschützt) | | `watch ` | Zusätzliche Policy-Liste beobachten | | `ban ` | **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 ` | Direkter, sofortiger Kick aus einem konkreten Raum (ohne Listen-Umweg) | | `rules` | Zeigt die Regeln einer Policy-Liste an | | `unban ` | 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 (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`, Funktion `decryptFile()` - der einzige Punkt im ganzen Client, an dem entschlüsselte Klartext-Bytes für *jeden* Anhangstyp entstehen (Bild/Audio/Video/Datei laufen alle über `MediaEventHelper` hier durch). Scan direkt nach dem Entschlüsseln, vor der Rückgabe als `Blob`. - **Versand**: `apps/web/src/ContentMessages.ts`, Funktion `uploadFile()` - 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` (neue `scanContent()`- Funktion + `ContentScanRejectedError`), von beiden Stellen genutzt. Fehlertexte über die bereits bestehenden Error-Rendering-Pfade in `MImageBody.tsx`/`MAudioBody.tsx`/ `VideoBodyViewModel.ts`/`FileBodyViewModel.ts` (gleiches Muster wie die schon vorhandenen `DecryptError`/`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 ` ohne Artefakt): lädt ein offiziell von `element-hq/element-web` signiertes Release-Tarball herunter - **Upstream, ohne unsere Patches**. - **Mit eigenem Build** (`webapp-artifact`-Mechanismus in `build_desktop_prepare.yaml`, gedacht für CI): würde unseren eigenen `apps/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](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/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.