Skip to content

Connector-Channel & Drift-Schutz

Status: LIVE seit 2026-05-20

Dieses Dokument beschreibt, wie der prilog-matrix-connector (das Python-Modul, das Synapse den JWT-Login + Room-Policy-Hooks beibringt) produktiv ausgerollt wird, und welche Schutzschichten dafür sorgen, dass eine kaputte Connector-Version den Kunden-Login nicht abschalten kann.

Es richtet sich an alle, die am Backend, an Tenant-Boxen oder am Reconcile-System arbeiten — und an Neue, die im Krisenfall die Heilungsroutine durchlaufen müssen.

Hintergrund: warum diese Architektur überhaupt existiert

Synapse spricht für jeden Login-Request seine Module-API an. Wir haben dort den prilog-matrix-connector eingehängt, der zwei Dinge tut:

  1. JWT-Login-Provider — akzeptiert HS256-JWTs, die das Backend mintet (siehe Synapse JWT-only Login). Ohne diesen Hook akzeptiert Synapse nur klassische Passwörter (oder im JWT-only-Modus gar keinen Login).
  2. on_create_room-Hook — leitet jede Raum-Erstellung an das Prilog-Backend zur Policy-Prüfung weiter.

Der Connector wird als Bind-Mount in den Synapse-Container gehängt:

host:    /srv/tenants/<slug>/connectors/prilog-matrix-connector/src
                ↓ (bind-mount)
container: /modules/prilog-matrix-connector/src
                ↓ (PYTHONPATH)
synapse:   import prilog_matrix_connector

Beim Reconcile (oder beim Provisioning eines neuen Tenants) wird der Connector aus einem Tarball auf api.prilog.chat per scp + tar -xzf auf die Tenant-Box geschrieben. Genau in dieser Pipeline liegt das Risiko, gegen das die folgenden Schichten schützen.

Was 2026-05-20 schiefging — als konkretes Beispiel

Am 20. Mai 2026 fielen alle Logins auf drei Shared-Tenants aus mit der Meldung „Matrix-Token ungültig oder abgelaufen". Die Ursache war eine Kette:

  • Das „Latest"-Tarball für den Connector (prilog-matrix-connector-latest.tar.gz) war auf eine alte Version f0480ea zurückgefallen. Diese Version registriert nur den on_create_room-Hook — keinen JWT-Login-Provider.
  • Der Reconcile-Cron um 03:30 UTC sah einen Hash-Drift („Container hat Version X, Latest ist Y") und installierte die ältere Version auf allen Tenants.
  • Damit war der JWT-Login-Pfad weg. Das Backend bekam M_FORBIDDEN: Invalid username or password zurück und übersetzte das in INVALID_MATRIX_TOKEN.

Sechs Schutzschichten — alle additiv, jede einzelne hätte den Vorfall verhindert — sorgen heute dafür, dass das nicht mehr passieren kann.

Die sechs Schutzschichten

1. CI-Build mit Sanity-Check

Repo: brasilspace/prilog-matrix-connector, Workflow .github/workflows/build.yml.

Jeder Push auf main:

  • prüft, ob src/prilog_matrix_connector/module.py den String register_password_auth_provider_callbacks enthält. Fehlt er, bricht der Build mit GitHub-Action-Error → kein Tarball wird hochgeladen.
  • packt einen Tarball prilog-matrix-connector.tar.gz und schreibt ihn in das prilog-artifacts-Repo als zwei Dateien:
    • prilog-matrix-connector-<sha>.tar.gz (immutable, versioniert)
    • prilog-matrix-connector-latest.tar.gz (überschrieben pro Push)

Wichtig: CI fasst die Datei prilog-matrix-connector-stable.tar.gz nie an. Nur ein Mensch via Promote-Skript darf sie setzen.

2. Promote-Skript mit Sanity-Check

Datei: /var/www/backend-api/scripts/promote-connector.sh auf api.prilog.chat.

So sieht der Promote-Vorgang aus:

bash
# Verfügbare Versionen anzeigen
bash /var/www/backend-api/scripts/promote-connector.sh --list

# Aktuelle Production-Version anzeigen
bash /var/www/backend-api/scripts/promote-connector.sh --current

# Eine konkrete Version als stable freigeben
bash /var/www/backend-api/scripts/promote-connector.sh <sha>

# Den aktuellen latest-Stand als stable freigeben (selten — meist will man explizit)
bash /var/www/backend-api/scripts/promote-connector.sh latest

Vor dem Symlink-Setzen extrahiert das Skript das Ziel-Tarball nach /tmp und prüft erneut, ob module.py den JWT-Hook enthält. Fehlt er, bricht es mit Exit-Code 2 und der Symlink bleibt unverändert. Auch ein Mensch kann also keine Login-tote Version produktiv setzen.

Nach erfolgreichem Promote:

bash
pm2 restart backend-api      # leert den 60s-Cache des Drift-Detectors
cd /var/www/backend-api && npx tsx scripts/reconcile-tenant.ts --all

(Ohne Eingriff zieht der nächste Reconcile-Cron um 03:30 UTC automatisch nach.)

3. stable- vs latest-Channel

Backend-Code: src/services/tenant-reconcile/connector-artifact.ts (Drift-Erkennung) und fix-operations.ts installMatrixConnector() (Installation).

Beide Pfade lesen jetzt:

1. /var/www/prilog-artifacts/matrix-connector/prilog-matrix-connector-stable.tar.gz
   ↓ (Fallback nur, wenn stable noch nie gesetzt wurde)
2. /var/www/prilog-artifacts/matrix-connector/prilog-matrix-connector-latest.tar.gz

Sobald promote-connector.sh einmal lief, ist stable gesetzt und latest wird vom Backend ignoriert. CI darf latest weiter rotieren (für Tests oder als Quelle für den nächsten Promote), ohne dass irgendein Tenant davon angefasst wird.

4. Drift-Detector-Sanity-Check

Datei: src/services/tenant-reconcile/connector-artifact.ts, Funktion getExpectedConnectorModuleSha256().

Vor jedem Drift-Vergleich liest das Service das Soll-Tarball ein und prüft im Inhalt der module.py, ob der JWT-Auth-Provider-Hook enthalten ist. Fehlt er, gibt die Funktion null zurück — der Drift-Detector überspringt dann den Connector-Check vollständig statt einen Rollback auf eine kaputte Version anzustoßen.

Zusätzlich wird sofort eine Mail an die Ops-Adresse geschickt (siehe Schicht 6):

[Prilog Ops] Connector-Artifact fehlt der JWT-Auth-Provider-Hook

Heißt: Selbst wenn jemand stable direkt überschreiben würde (was das Promote-Skript verhindert, aber theoretisch geht), sieht das Backend „Kein gültiges Sollbild verfügbar" → kein automatischer Schaden.

5. Reconcile-Smoke mit Auto-Rollback

Datei: src/services/tenant-reconcile/fix-operations.ts, Funktion installMatrixConnector().

Nach jedem tar -xzf läuft automatisch:

  1. docker compose -f /srv/tenants/<slug>/docker-compose.yml restart synapse — Bind-Mount-Inode wird neu aufgelöst (sonst zeigt der Container auf das alte, jetzt verwaiste Verzeichnis).
  2. jwtLoginSmoke(tenantId) — mintet ein Test-JWT, sendet m.login.password an den Synapse-Container, erwartet HTTP 200 + access_token.
  3. Bei Smoke-Fehler: Rollback. Die alte Version (/srv/tenants/<slug>/connectors/prilog-matrix-connector.old) wird wieder aktiviert, Synapse erneut restartet, eine Mail an Ops geht raus. Der Reconcile meldet Fixes: 0 applied, 1 failed.
  4. Bei Smoke-Erfolg: .old-Verzeichnis wird aufgeräumt, Reconcile meldet Fixes: 1 applied, JWT-Login-Smoke OK.

Das ist der Sicherheitsgurt. Selbst wenn vier Schichten versagen würden und das Backend wirklich versucht, eine kaputte Version zu installieren — diese Schicht stellt sicher, dass der Tenant nach dem Reconcile mindestens so funktionsfähig ist wie vorher.

6. JWT-Login-Watch alle 30 Minuten

Datei: src/services/login-watch.service.ts, Cron-Eintrag in src/core/cron/jobs.ts als jwt-login-watch.

Cron-Schedule: */30 * * * * (alle 30 Minuten).

Pro Tenant (mit gesetztem synapse_jwt_secret — dedicated Hosts ohne JWT-Pfad werden übersprungen):

  1. synapseLoginViaJwt(tenantId, adminUsername) — gleiche Funktion wie im echten Login-Endpunkt.
  2. Sofort /_matrix/client/v3/logout mit dem frisch erhaltenen Token. Das ist wichtig, weil sonst pro Smoke ein Device + Access-Token in der Synapse-DB hängenbleibt (über ein Jahr wären das ~50 000 Devices pro Tenant).
  3. Bei Fehler: Zähler hochzählen. Erst nach 2 Fehlern in Folge geht eine Mail an Ops — verhindert Spam bei einmaligen Netzwerk-Glitches.
  4. Bei Erholung (nach Fehler-Serie wieder ok): eine „Recovered"-Mail.

Last-Messung produktiv: ~590 ms wall-time für alle vier Tenants pro Tick — also etwa eine Sekunde Aktivität pro Stunde. Auf die CPU schlägt das nicht messbar durch.

Heilungs-Routine: was tun, wenn Login wieder stirbt

Die Symptome zur Erkennung:

  • User-Meldung: „Matrix-Token ungültig oder abgelaufen" oder „Benutzername oder Passwort falsch" trotz korrekter Eingabe.
  • pm2 logs backend-api zeigt INVALID_MATRIX_TOKEN-Antworten.
  • Cron-Mail vom JWT-Login-Watch ist eingegangen.

Schritt-für-Schritt-Heilung:

bash
# 1. Aktueller stable-Stand
bash /var/www/backend-api/scripts/promote-connector.sh --current

# 2. Verfügbare Versionen
bash /var/www/backend-api/scripts/promote-connector.sh --list

# 3. Eine bekannt-gute Version promoten (Sanity-Check schützt vor falscher Wahl)
bash /var/www/backend-api/scripts/promote-connector.sh <sha>

# 4. Backend-Cache leeren
pm2 restart backend-api

# 5. Rollout auf alle Tenants — Smoke + Rollback laufen automatisch pro Tenant
cd /var/www/backend-api && npx tsx scripts/reconcile-tenant.ts --all

Falls das nichts hilft (Connector ok, aber Login schlägt trotzdem fehl), prüfen:

  • Passwort-Hash in der Tabelle matrix_user_passwords: möglicherweise veraltet. Mit bcrypt.hash(plainPassword, 12) neu setzen und failed_attempts = 0, locked_until = null.
  • Rate-Limit: nach zu vielen Fehlversuchen sperrt das Backend den Account temporär. Gleiche Tabelle, gleiches Feld.
  • Synapse-Container läuft? docker ps --filter name=synapse-<slug> — Status healthy erwartet.
  • Bind-Mount sichtbar? docker exec synapse-<slug> python -c 'import prilog_matrix_connector; print(prilog_matrix_connector.__file__)' muss ok: /modules/... ausgeben.

Diagnose-Befehle

bash
# Stable-Symlink prüfen
ls -la /var/www/prilog-artifacts/matrix-connector/prilog-matrix-connector-stable.tar.gz

# Welche Connector-Version läuft pro Tenant?
ssh root@<host> "docker exec synapse-<slug> grep -c register_password_auth_provider_callbacks /modules/prilog-matrix-connector/src/prilog_matrix_connector/module.py"
# Erwartet: >= 1

# Smoke-Login direkt gegen Synapse (mit echtem JWT-Secret aus tenant_settings)
# - bei lebendem Connector + ungültigem JWT: HTTP 400 oder M_FORBIDDEN
# - bei totem Connector: M_UNKNOWN "Unknown login type m.login.password"
curl -X POST "https://<slug>.prilog.team/_matrix/client/v3/login" \
  -H 'content-type: application/json' \
  -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"admin"},"password":"x"}'

# Reconcile-Dry-Run (zeigt aktuellen Drift-Stand pro Tenant, ohne zu fixen)
cd /var/www/backend-api && npx tsx scripts/reconcile-tenant.ts --tenant=<slug> --dry-run

# Login-Watch manuell triggern (für Verifikation nach Eingriff)
cd /var/www/backend-api && npx tsx -e "import('./src/services/login-watch.service.js').then(m => m.runLoginWatch().then(console.log))"

Was bei neuen Tenants automatisch greift

Beim Provisioning eines neuen Tenants:

  1. synapse-jwt-provision.service legt synapse_jwt_secret in tenant_settings an und schreibt das modules:-Block in die homeserver.yaml.
  2. Der Connector wird aus dem stable-Channel installiert (Fallback latest, falls noch nie promoted).
  3. Die Provisioning-Smoke-Suite (tenant-smoke/index.ts) prüft sechs Connector-relevante Probes, darunter connector_module_importable und synapse_jwt_login_works. Schlägt eine pflicht-Probe fehl, ist das Provisioning rot.
  4. Nach Provisioning übernimmt der 30-Min-Watch.

Heißt: Ein neuer Tenant geht überhaupt nur dann live, wenn der JWT-Login einmal funktioniert hat.

Referenz: betroffene Dateien

DateiZweck
prilog-backend-api/scripts/promote-connector.shConnector-Version als stable freigeben (mit Sanity-Check)
prilog-backend-api/src/services/tenant-reconcile/connector-artifact.tsErwarteten Connector-Hash + Sanity-Check liefern
prilog-backend-api/src/services/tenant-reconcile/fix-operations.tsinstallMatrixConnector mit Smoke + Rollback
prilog-backend-api/src/services/ops-alert.service.tsZentrale Ops-Mail mit 10-Min-Dedup
prilog-backend-api/src/services/login-watch.service.tsPeriodischer JWT-Login-Health-Check
prilog-backend-api/src/core/cron/jobs.ts (Eintrag jwt-login-watch)Cron-Registrierung
prilog-matrix-connector/.github/workflows/build.ymlCI mit JWT-Hook-Sanity-Check + Push in prilog-artifacts

Verwandte Doku