ClamAV-Antivirus-Gateway — Implementation
Status: alle drei Phasen LIVE seit 2026-05-09
- Phase 1 — Scan-Pipeline + DMS/Mein-Fach/Space-File-Hooks + Admin-UI ✓
- Phase 2 — SHA-256-Whitelist pro Tenant, Schul-Admin-Workflow ✓
- Phase 3 — Quarantäne statt Sofort-Delete, Forensik-Download, 90d-Retention ✓
Anwender-Doku (für Schul-Sekretariat): siehe Virenschutz im Schul-Alltag.
ClamAV scannt jeden Upload (DMS, Mein Fach, Space-Datei, Personal-Fach-Drop) bevor er in der DB als Dokument registriert wird. Bei einem Treffer wandert die Datei in einen Quarantäne-Pfad, ein Audit-Eintrag in av_incidents wird geschrieben, und der Web-Client zeigt einen klaren Toast mit der Virus-Signatur.
Architektur
Drei Komponenten:
┌────────────────────────┐
│ prilog-clamav │
│ cpx22 (4GB), nbg1 │
│ Tailscale 100.64.x.x │
│ │
│ Docker: │
│ clamav/clamav:stable │
│ → 0.0.0.0:3310 │
│ → freshclam täglich │
└───────────▲────────────┘
│
Tailscale│Mesh, port 3310
│
┌────────────────────────────┴──────────────────────────────┐
│ Backend-API (shared-1) │
│ │
│ CLAMD_HOST=100.64.174.15 CLAMD_PORT=3310 │
│ │
│ lib/clamav.ts ────────► scanBuffer (INSTREAM-Protocol) │
│ services/av-scan ─────► scanS3Object (Glue zu MinIO) │
│ services/av-incident ─► recordAvIncident, listAvIncidents│
│ │
│ Upload-Routes (4): │
│ - DMS Document confirm-upload │
│ - DMS Document version confirm │
│ - Space-Datei confirm-upload │
│ - Space-Datei version confirm │
│ + bestehend: Personal-Fach (war schon vorher integriert) │
│ │
│ Bei Treffer: HTTP 422 + details.signature │
└───────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ Web-Client │
│ │
│ core/upload/upload-error.ts → showUploadError(err) │
│ Bei 422+signature: │
│ toast.error('🛡️ Malware erkannt — Datei abgelehnt │
│ (Signatur: Eicar-Test-Signature)') │
└───────────────────────────────────────────────────────────┘Warum dedizierter Server statt shared-1-Container?
ClamAV ist ein stateful Service:
- 2 GB RAM konstant für die Signatur-DB (3.6 Mio Signaturen aus daily.cvd
- main.cvd in-memory)
- CPU-Spikes bei jedem Scan (50–100 MB/s pro Core)
- Update-Last jede Nacht durch
freshclam(~100 MB Download, In-Memory-Reload)
Pattern-Konsistenz mit Whisper (eigener Server für ML-Modell + GPU-Last). Wenn das Backend-API auf shared-1 unter Last steht, sollen ClamAV-Spikes das nicht beeinflussen — und umgekehrt. Migration zu eigenem Server kostet ~4€/Monat (cpx22) und ist die saubere Lösung.
Provisionierung
Server wurde am 2026-05-09 provisioniert:
Name: prilog-clamav
Hetzner-ID: 130012871
Type: cpx22 (2 vCPU AMD, 4 GB RAM)
Location: nbg1 (Nürnberg)
Public-IP: 91.98.230.246
Tailscale: 100.64.174.15 (Hostname: prilog-clamav)
Firewall: SSH:22 + ICMP von Internet erreichbar,
Port 3310 nur von Tailscale-Range 100.64.0.0/10Cloud-init installiert: Docker + Tailscale (mit Auth-Key) + ClamAV-Container mit network_mode: host und --restart=unless-stopped. Initial-Signatur-Update via freshclam läuft beim ersten Start (~3-5 Min), danach automatisch 4× täglich.
Datenmodell
av_incidents (Migration 0066):
| Spalte | Typ | Beschreibung |
|---|---|---|
id | bigserial | PK |
tenant_id | varchar(64) | Tenant-ID für Multi-Tenant-Filter |
user_matrix_id | varchar(255)? | wer hat hochgeladen |
filename | varchar(500) | Dateiname |
virus_signature | varchar(255) | ClamAV-Signatur, z.B. Eicar-Test-Signature |
context | varchar(30) | dms / mein-fach / space-file / chat / other |
size_bytes | bigint? | Datei-Größe (vor Löschung) |
mimetype | varchar(100)? | MIME-Type |
sha256 | varchar(64)? | Hash (falls vorhanden, für Whitelist-Lookup) |
blocked | boolean | TRUE = Datei wurde abgewiesen, FALSE = warning-only |
quarantine_key | varchar(500)? | Falls Quarantäne-Storage genutzt wird (heute leer) |
detected_at | timestamptz | Zeitpunkt des Treffers |
Indizes: tenant+detected_at, user, signature, context.
Failopen-Default
Wenn clamd nicht erreichbar ist (Tailscale down, Container neu startet, Wartung), läuft das System failopen: Uploads werden akzeptiert, ohne Scan-Garantie. Die Logs zeigen [scan] uebersprungen: clamd offline.
Begründung: lieber Upload akzeptieren als gar nicht ankommen. Schul-Alltag hat einen niedrigen Threat-Level (überwiegend Klassen-PDFs, Eltern-Fotos), und ein 30-Sekunden-clamd-Restart soll nicht den ganzen Schulbetrieb blockieren.
Bei strikteren Anforderungen kann das Verhalten umgekehrt werden: CLAMAV_FAIL_MODE=closed — dann gibt's bei clamd-Ausfall HTTP 503 statt clean-Pass. Heute: failopen.
Smoke-Test
Aus dem /var/www/backend-api-Verzeichnis auf shared-1:
// eicar-test.mjs
import { scanBuffer, isClamdAvailable } from "./dist/lib/clamav.js";
const EICAR = Buffer.from('X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*');
const CLEAN = Buffer.from('Hallo, das ist eine harmlose Textdatei.');
console.log('clamd available:', await isClamdAvailable());
console.log('EICAR scan:', JSON.stringify(await scanBuffer(EICAR)));
console.log('clean scan:', JSON.stringify(await scanBuffer(CLEAN)));Erwartete Ausgabe:
clamd available: true
EICAR scan: {"scanned":true,"clean":false,"signature":"Eicar-Test-Signature"}
clean scan: {"scanned":true,"clean":true}EICAR ist die Standard-Test-Signatur — eine harmlose Zeichenfolge die jeder Virus-Scanner als Treffer erkennen muss.
Live-Test im Browser
- EICAR-Datei runterladen (https://secure.eicar.org/eicar.com)
- Im Web-Client zu Mein Fach oder zu einem Space mit Datei-Upload
- Datei ins Drop-Target ziehen
- Toast erscheint: 🛡️ Malware erkannt — Datei abgelehnt (Eicar-Test-Signature)
- Datei taucht nicht in der Liste auf
- Im Admin:
/av-incidentszeigt den Eintrag mit Tenant, User, Kontext, Zeitstempel
Was die Pipeline NICHT macht
Synapse-Direct-Media-Uploads (legacy chat upload via
/_matrix/media/v3/upload) laufen nicht durch den Backend-Scan, weil sie direkt zu Synapse gehen. Variante A aus dem Original-Plan (nginx Pre-Upload-Hook) ist möglich, aber durch die DMS-Unification wandern Chat-Anhänge zunehmend ins DMS — der Synapse-Direct-Pfad wird verdrängt.Bestehende Daten vor dem 2026-05-09 wurden nicht nachträglich gescannt. Bei Bedarf: Scan-Cron, der alle
DocumentundFileItem-Records iteriert und scannt. Heute nicht implementiert.Whitelist für False-Positives. Wenn ein Upload fälschlich als infiziert markiert wird (selten, aber möglich bei Heuristik-Treffern), gibt's heute keinen UI-Knopf zum Whitelisten. Workaround: ClamAV-Signatur in
clamd.confals ignored aufnehmen, Container neu starten.
Operational
- clamd-Status: Admin-UI
/av-incidentszeigt oben rechts grünen Schild bei Online, roten bei Offline. - Container-Restart:
ssh root@91.98.230.246 'docker restart clamav' - Signature-Update manuell: passiert automatisch alle 6h, manuell:
ssh root@91.98.230.246 'docker exec clamav freshclam' - Logs:
ssh root@91.98.230.246 'docker logs clamav --tail 100' - Skalierung: bei >100 Tenants oder hoher Upload-Frequenz: zweite ClamAV-Instanz hinter HAProxy, oder größeren Hetzner (cpx32, 8 GB RAM, 4 vCPU). Aktuell cpx22 reicht problemlos für 50+ Tenants.
Phase 2 (LIVE 2026-05-09): SHA-256-Whitelist pro Tenant
Tenant-Admins können bei einem False-Positive den Hash der Datei whitelisten:
- Migration 0067
av_whitelist(tenantId + sha256 unique, plus filename, reason, createdBy, useCount, lastUsedAt) - Vor dem clamd-Scan: SHA-256 server-side berechnen, Whitelist-Lookup. Bei Match: Scan überspringen + Audit-Eintrag mit
blocked=false - Workspace-API:
GET/POST/DELETE /workspace/av-whitelist - Web-Client (Settings → Sicherheit): zwei neue Blöcke. Bei einem geblockten Vorfall mit sha256 → Button "Whitelisten" mit Begründungs-Prompt
Phase 3 (LIVE 2026-05-09): Quarantäne statt Sofort-Delete
Statt infizierte Dateien direkt zu löschen, werden sie in einen Quarantäne-Pfad verschoben. Tenant-Admin kann sie zur Forensik runterladen oder endgültig löschen.
moveToQuarantine(tenantId, sourceKey)in object-storage.service: S3-CopyObject inquarantine/<ts>_<orig-key>+ DeleteObject vom Original. Atomare Move-Semantikav_incidents.quarantine_keywird beim Treffer mit dem neuen Key gefüllt- Workspace-Endpoints:
GET /workspace/av-incidents/:id/quarantine-download?reason=…— Forensik-Download mit Pflicht-Begründung (>=5 Zeichen). Audit-Eintrag mitFORENSIC_DOWNLOAD: <reason>DELETE /workspace/av-incidents/:id/quarantine— endgültige Löschung. Audit bleibt
- Web-Client: zwei neue Icon-Buttons pro Vorfall (Download + Endgültig löschen)
- Cron
av-quarantine-retentiontäglich 04:15 UTC: löscht Quarantäne-Dateien älter als 90 Tage. Audit bleibt
Lifecycle einer infizierten Datei:
Upload → Scan → Treffer
↓
moveToQuarantine: <orig-key> → quarantine/<ts>_<orig-key>
↓
av_incidents.quarantine_key = <neuer Pfad>
↓
[bis zu 90 Tage]
↓
Optionen:
a) Tenant-Admin: Download (Forensik, Audit)
b) Tenant-Admin: Endgültig löschen
c) Cron: automatischer Cleanup nach 90 TagenFolge-Stufen (geplant)
- Phase 4: Cron-basierter Re-Scan aller bestehenden Dokumente bei großem Signatur-Update (z.B. neue CVE-Disclosure)
- Phase 5: Synapse-Media-Direct-Pfad mit nginx Pre-Hook absichern (oder DMS-Unification verdrängt diesen Pfad komplett)
- Restore-Workflow für False-Positives (heute: Whitelist + manueller Re-Upload). Saubere Lösung: Datei aus Quarantäne in einen
recovered/-Pfad verschieben + automatischen Whitelist-Eintrag