Matrix Teil 2: Element Web, Föderation und Admin-Alltag
Element Web neben Synapse auf derselben Domain, Geräteverifizierung, verschlüsselte Räume, Einladungstokens und die Admin-API für Konten, Räume und Medien.
Inhaltsverzeichnis
Der Server aus Teil 1 läuft, föderiert und hat genau ein Konto. Nur sieht ihn niemand. Jetzt kommt die Oberfläche dazu, und mit ihr die Fragen, die im Alltag wirklich auftauchen: Wie kommen Freunde rein, ohne dass die Registrierung offensteht? Wer räumt die Räume auf?
Was bauen wir?
Wir stellen Element Web 1.12.33 neben Synapse 1.164.2, und zwar auf derselben Domain; Traefik teilt die Anfragen nach Pfad auf. Danach hast du eine fertige Chat-Oberfläche im Browser, einen Ende-zu-Ende-verschlüsselten Raum, eine Registrierung, die nur mit Einladungstoken funktioniert, und die Admin-Handgriffe für Konten, Räume und Medien.
Das ist Teil 2 der Matrix-Serie. Teil 1 hat den Homeserver, die Delegation und die Föderation aufgebaut; hier geht es um alles, was danach kommt.
Warum eine Domain und nicht zwei
matrix. für den Server, chat. für Element) sind der üblichere Weg und völlig
in Ordnung. Wir machen es hier bewusst auf nur einer Domain, weil das der schwierigere Fall ist:
Sobald sich zwei Dienste einen Hostnamen teilen, entscheidet die Reihenfolge der Traefik-Regeln
darüber, wer eine Anfrage bekommt, und genau da geht es in der Praxis schief. Wer zwei Subdomains
hat, überspringt Schritt 1 und lässt in Schritt 3 die priority-Zeilen weg.Voraussetzungen
- Ein laufender Matrix-Server aus Matrix: der eigene Chat-Server mit Synapse (Teil 1), inklusive grünem Föderationstest
- Debian 13 mit Docker und einem Traefik, der die Zertifikate holt
- Die Domain aus Teil 1; der
server_namebleibt unverändert - Etwa 60 Minuten, davon ein guter Teil für den ersten Login
VPS 2000 G12.5
8 vCore · 16 GB RAM · 256 GB SSD
ab 26,92 €/Monat
Synapse wächst mit dem ersten großen föderierten Raum von gut 120 auf rund 280 MB. Element selbst braucht nichts – unter 5 MB.
💶 1 Monat gratis:
Einmal einlösbar, gilt für VPS 2000 G12.5. Im Warenkorb einlösen →
💶 5,95 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite
Schritt für Schritt
Schritt 1: Wie sich zwei Dienste einen Hostnamen teilen
Synapse beansprucht drei Pfadpräfixe und sonst nichts:
/_matrix # die Client- und Föderations-API
/_synapse # die Verwaltungs-API
/.well-known/matrix # die Delegation aus Teil 1Alles andere (die Startseite, JavaScript, Bilder, Schriften) gehört Element. Traefik kann das
auseinanderhalten, wenn man ihm sagt, welche Regel zuerst geprüft wird. Dafür gibt es
priority: Je höher die Zahl, desto früher wird die Regel ausgewertet.
Regel priority Ergebnis
Host(`DEINE_DOMAIN`) && PathPrefix(`/_matrix`) 100 Synapse
Host(`DEINE_DOMAIN`) 10 ElementOhne priority sortiert Traefik nach Länge der Regel, und die längere gewinnt. Das führt hier
zufällig zum richtigen Ergebnis. Verlassen kannst du dich darauf nicht, denn das kann sich mit
jeder zusätzlichen Regel ändern. Schreib die Zahlen hin.
Schritt 2: Element konfigurieren
Element Web ist reines JavaScript im Browser; die gesamte Einstellung steckt in einer einzigen
config.json. Leg sie neben die compose.yaml:
cd /opt/matrix
nano element-config.json{
"default_server_config": {
"m.homeserver": {
"base_url": "https://DEINE_DOMAIN",
"server_name": "DEINE_DOMAIN"
}
},
"brand": "Kochabend Chat",
"disable_custom_urls": true,
"disable_guests": true,
"disable_3pid_login": true,
"default_country_code": "DE",
"show_labs_settings": false,
"room_directory": { "servers": ["DEINE_DOMAIN"] },
"setting_defaults": {
"breadcrumbs": true,
"UIFeature.feedback": false,
"UIFeature.registration": false
}
}Die wichtigen Schalter:
disable_custom_urls: true: Die Anmeldemaske zeigt deinen Server fest an, statt ein Eingabefeld für beliebige Homeserver. Ohne das melden sich Leute versehentlich bei matrix.org an und wundern sich, warum ihr Konto nicht existiert.disable_3pid_loginunddisable_guests: Kein Login über E-Mail/Telefon (dafür bräuchte es einen Identitätsserver) und keine Gastkonten.UIFeature.registration: false: Der Link „Konto erstellen" verschwindet. Die Registrierung regeln wir in Schritt 7 über Tokens, nicht über einen offenen Knopf.show_labs_settings: false: Element hat eine ganze Schublade unfertiger Funktionen. Auf einem Server, den andere mitbenutzen, bleibt die besser zu.
Die Datei muss für den Container lesbar sein:
chmod 0644 element-config.jsonWenn du den Server nach Lynis gehärtet hast
umask 027 in /etc/login.defs, wie es das
Sicherheits-Audit mit Lynis vorschlägt, erzeugt neue
Dateien mit 0640. Element läuft im Container unentprivilegiert und kann die Datei dann nicht
lesen; der Container startet mit cp: can't open '/app/config.json': Permission denied gar nicht
erst. Das chmod ist deshalb Pflicht.Schritt 3: Den Stack erweitern
In der compose.yaml aus Teil 1 ändern wir die Traefik-Regel von Synapse und hängen einen zweiten
Dienst an:
# /opt/matrix/compose.yaml (Auszug)
synapse:
image: ghcr.io/element-hq/synapse:v1.164.2
# ... unverändert ...
labels:
- traefik.enable=true
- traefik.http.routers.synapse.rule=Host(`DEINE_DOMAIN`) && (PathPrefix(`/_matrix`) || PathPrefix(`/_synapse`) || PathPrefix(`/.well-known/matrix`))
- traefik.http.routers.synapse.priority=100
- traefik.http.routers.synapse.entrypoints=websecure
- traefik.http.routers.synapse.tls.certresolver=le
- traefik.http.services.synapse.loadbalancer.server.port=8008
element:
image: vectorim/element-web:v1.12.33
container_name: element
restart: unless-stopped
volumes:
- ./element-config.json:/app/config.json:ro
networks: [proxy]
labels:
- traefik.enable=true
- traefik.http.routers.element.rule=Host(`DEINE_DOMAIN`)
- traefik.http.routers.element.priority=10
- traefik.http.routers.element.entrypoints=websecure
- traefik.http.routers.element.tls.certresolver=le
- traefik.http.services.element.loadbalancer.server.port=80Element hängt nur im proxy-Netz. Es spricht nie mit der Datenbank und auch nicht direkt mit
Synapse: Die API-Aufrufe macht der Browser des Besuchers, nicht der Container. Das interne Netz
aus Teil 1 bleibt der Datenbank vorbehalten.
docker compose up -d
docker compose psNAME STATUS
element Up 20 seconds (healthy)
matrix-db Up 20 seconds (healthy)
synapse Up 20 seconds (healthy)Schritt 4: Beide Wege gegenprüfen
Bevor du den Browser aufmachst, prüf von außen, dass die Aufteilung wirklich greift. Erst die Wurzel, hier muss Element antworten:
curl -s https://DEINE_DOMAIN/ | grep -o "<title>[^<]*</title>"<title>Element</title>Dann die Konfiguration, die Element beim Start lädt:
curl -s https://DEINE_DOMAIN/config.json | head -5{
"default_server_config": {
"m.homeserver": {
"base_url": "https://DEINE_DOMAIN",Und schließlich, dass Synapse seine drei Pfade behalten hat:
curl -s -o /dev/null -w "%{http_code}\n" https://DEINE_DOMAIN/_matrix/client/versions
curl -s https://DEINE_DOMAIN/.well-known/matrix/server
curl -s https://DEINE_DOMAIN/.well-known/matrix/client200
{"m.server":"DEINE_DOMAIN:443"}
{"m.homeserver":{"base_url":"https://DEINE_DOMAIN/"}}Kommt bei einem der drei Aufrufe HTML statt JSON zurück, hat Element die Anfrage abgefangen. Dann
stimmt die priority nicht. Kommt beim ersten Aufruf JSON statt HTML, ist es umgekehrt.
Schritt 5: Der erste Login und die Geräteverifizierung
Ruf https://DEINE_DOMAIN/ im Browser auf. Weil disable_custom_urls gesetzt ist, steht dein
Server fest in der Maske; du gibst nur Benutzernamen und Passwort aus Teil 1 ein.

Beim allerersten Gerät richtet Element die Verschlüsselung selbst ein, ohne Rückfrage: Es erzeugt im Hintergrund einen Cross-Signing-Schlüssel, der deine Geräte als deine ausweist, und eine Schlüsselsicherung auf dem Server. Der Server speichert davon nur verschlüsselte Kopien; lesen kann er sie nicht. Zu sehen ist davon nur ein Hinweis oben links:

Den Hinweis auf die Schlüsselsicherung nicht wegklicken
Der Hinweis „Back up your chats" sagt: „Your chats are backed up automatically. You’ll need your recovery key to restore them if you lose all your devices." Die Sicherung läuft also schon, aber ohne Wiederherstellungsschlüssel kommst du nie wieder an sie heran.
Nimm Continue. Element öffnet die Einstellungen unter „Encryption"; dort führt „Get recovery key" und „Continue" zu „Save your recovery key somewhere safe" mit dem Schlüssel. Der gehört in den Passwortmanager. Wer stattdessen auf „Dismiss" klickt, hat keinen.
Das fällt erst auf, wenn du dich auf einem weiteren Gerät anmeldest. Dann fragt Element mit
„Confirm your digital identity": „Use another device" oder „Use recovery key". Ohne Schlüssel
ist die zweite Möglichkeit ausgegraut, und sind alle alten Geräte weg, bleibt nur „Can’t
confirm?". Das setzt deine Identität zurück, und der Verlauf, der nur auf dem Server liegt, ist
nicht mehr zu entziffern. Auch nicht von dir als Serveradministrator: Ein pg_dump sichert die
Nachrichten, nicht die Schlüssel dazu.
Schritt 6: Der erste verschlüsselte Raum
Über „New room" legst du einen Raum an. Zwei Einstellungen entscheiden hier über Dinge, die später feststehen:

- „Invite only": Die Sichtbarkeit lässt sich später ändern.
- „Enable end-to-end encryption": Das lässt sich nicht zurücknehmen. Element sagt es auch dazu: „You can’t disable this later. Bridges & most bots won’t work yet." Ein verschlüsselter Raum ist für die meisten Brücken zu anderen Netzen und für viele Bots nicht nutzbar. Wenn du später ein Telegram- oder IRC-Bridging planst, leg dafür einen unverschlüsselten Raum an.
Danach steht der Raum, und die erste Nachricht geht raus:

Über die Admin-API kannst du gegenprüfen, dass der Server den Raum tatsächlich als verschlüsselt führt:
curl -s "https://DEINE_DOMAIN/_synapse/admin/v1/rooms?limit=5" \
-H "Authorization: Bearer DEIN_TOKEN"name: Kochabend | members: 1 | encryption: m.megolm.v1.aes-sha2m.megolm.v1.aes-sha2 ist das Gruppenverschlüsselungsverfahren von Matrix. Steht dort null, ist
der Raum offen lesbar, für den Server und für jeden, der die Datenbank in die Finger bekommt.
Schritt 7: Freunde einlassen, ohne die Registrierung zu öffnen
In Teil 1 steht enable_registration: false. Das ist richtig, solange du allein bist. Nur müsstest
du jedes weitere Konto von Hand anlegen und das Passwort per Chat verschicken. Der bessere Weg sind
Registrierungstokens: Die Registrierung ist offen, aber nur mit einem Einladungscode, den du
ausgibst.
# /opt/matrix/data/homeserver.yaml
enable_registration: true
registration_requires_token: truedocker compose restart synapseJetzt legst du einen Token an, hier einen, der dreimal benutzt werden darf:
curl -s -X POST "https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens/new" \
-H "Authorization: Bearer DEIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uses_allowed": 3, "length": 12}'{"token":"LvM3cFXHXX7H","uses_allowed":3,"pending":0,"completed":0,"expiry_time":null}Diese Zeichenfolge schickst du deinen Leuten. Element fragt sie während der Registrierung ab. Dass ohne Token nichts geht, prüfst du am einfachsten am Anfang des Registrierungsvorgangs:
curl -s -X POST "https://DEINE_DOMAIN/_matrix/client/v3/register" \
-H "Content-Type: application/json" -d '{}'{"session":"PklWEdRKlnhHbBVJrwJnwnBG",
"flows":[{"stages":["m.login.registration_token","m.login.dummy"]}],"params":{}}Der Server verlangt jetzt zwei Stufen, und die erste ist der Token. Ohne ihn (oder mit einem falschen, dann meldet der Server „Invalid registration token") endet der Vorgang, ohne dass ein Konto entsteht. Nach einer erfolgreichen Registrierung siehst du den Verbrauch:
curl -s "https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens" \
-H "Authorization: Bearer DEIN_TOKEN"{"registration_tokens":[{"token":"LvM3cFXHXX7H","uses_allowed":3,"pending":0,"completed":1,"expiry_time":null}]}completed sind fertige Registrierungen, pending angefangene. Ist der Token aufgebraucht, läuft
er ins Leere; du musst nichts abschalten. Mit "expiry_time" (Millisekunden seit 1970) kannst du
ihm zusätzlich ein Ablaufdatum geben.
Schritt 8: Föderation im Alltag
Der Föderationstest aus Teil 1 prüft, ob andere deinen Server erreichen. Die andere Richtung, ob dein Server rauskommt, prüfst du, indem du nach jemandem fragst, den es nur woanders gibt:
curl -s "https://DEINE_DOMAIN/_matrix/client/v3/profile/@matthew:matrix.org" \
-H "Authorization: Bearer DEIN_TOKEN"{"displayname":"Matthew","avatar_url":"mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe"}Diese Antwort kann dein Server nicht selbst erfunden haben: Er hat matrix.org gefragt, die Signatur geprüft und das Ergebnis weitergereicht. Dasselbe funktioniert mit Raumadressen; der Verzeichniseintrag verrät zusätzlich, welche Server im Raum sitzen.
Was das kostet, zeigt der Beitritt zu einem echten öffentlichen Raum:
time curl -s -X POST "https://DEINE_DOMAIN/_matrix/client/v3/join/%23element-web:matrix.org" \
-H "Authorization: Bearer DEIN_TOKEN" -H "Content-Type: application/json" -d '{}'{"room_id":"!xUW4PpAe1CmThA3r2wI8IrgwwsK006-zqWdJCljpd10"}
real 0m2.221sZwei Sekunden. In den zwei Minuten danach, während Synapse den Raumzustand nachlädt (gut 5.000 Zustandsereignisse), steigt sein Arbeitsspeicher von rund 120 MB auf rund 280 MB, die Datenbank von 15 auf 63 MB. Für einen einzigen Raum. Das ist der wichtigste Satz über Matrix-Föderation: Dein Server verwaltet für jeden Raum, in dem du sitzt, dessen gesamten Zustand mit, auch den Teil, der von fremden Servern kommt. Ein Homeserver mit 1 GB RAM ist deshalb schnell am Ende, sobald jemand drei große Räume betritt.
Raum-IDs haben ihr Format geändert
!abcdef:matrix.org bleiben gültig. Skripte, die auf den Doppelpunkt bauen, tun es nicht.Schritt 9: Der Admin-Alltag
Drei Handgriffe brauchst du regelmäßig. Konten stilllegen. Ein Konto wird nie gelöscht, sondern deaktiviert, weil die Benutzer-ID in fremden Räumen weiterlebt:
curl -s -X POST "https://DEINE_DOMAIN/_synapse/admin/v1/deactivate/@NAME:DEINE_DOMAIN" \
-H "Authorization: Bearer DEIN_TOKEN" \
-H "Content-Type: application/json" -d '{"erase": true}'{"id_server_unbind_result":"success"}erase: true löscht zusätzlich Anzeigename und Profilbild. Die Benutzer-ID bleibt gesperrt und kann
nicht neu vergeben werden. Das ist Absicht, sonst könnte jemand die Identität übernehmen.
Räume loswerden, die du nicht mehr brauchst. Erst austreten und vergessen, dann vom Server räumen:
RAUM='!DEINE_RAUM_ID'
curl -s -X DELETE "https://DEINE_DOMAIN/_synapse/admin/v2/rooms/$RAUM" \
-H "Authorization: Bearer DEIN_TOKEN" \
-H "Content-Type: application/json" -d '{"purge": true, "block": false}'{"delete_id":"qUcHmFcjfVZFfHWv"}Der Aufruf kommt sofort zurück, das Aufräumen läuft im Hintergrund. Den Stand fragst du unter
/_synapse/admin/v2/rooms/delete_status/DELETE_ID ab, bis dort "status":"complete" steht. Mit
"block": true sperrst du den Raum zusätzlich, sodass niemand ihn erneut betreten kann.
Medien aufräumen. Jedes Bild aus jedem föderierten Raum landet als Kopie bei dir:
VORHER=$(( ($(date +%s) - 2592000) * 1000 ))
curl -s -X POST "https://DEINE_DOMAIN/_synapse/admin/v1/media/DEINE_DOMAIN/delete?before_ts=$VORHER" \
-H "Authorization: Bearer DEIN_TOKEN"{"deleted_media":[],"total":0}before_ts ist ein Zeitstempel in Millisekunden; die Rechnung oben ergibt „älter als 30 Tage".
Vergisst man die drei Nullen, löscht der Aufruf nichts, weil der Zeitpunkt 1970 liegt.
Die Admin-API gehört nicht ins offene Netz
/_synapse/admin im Reverse Proxy auf dein VPN oder deine
feste Adresse; das Muster dafür steht in
Dienste nur im VPN erreichbar machen.Wenn es nicht funktioniert
Unter /_matrix/client/versions kommt die Element-Startseite statt JSON. Beide Router hören auf denselben Host, und Traefik hat den falschen zuerst geprüft. Setz traefik.http.routers.synapse.priority=100 und …element.priority=10. Ohne ausdrückliche Zahlen sortiert Traefik nach Regellänge, und das Ergebnis ändert sich, sobald eine weitere Regel dazukommt. Nach dem Ändern reicht docker compose up -d; Traefik liest die Labels sofort neu.
Der Element-Container startet nicht und meldet cp: can't open '/app/config.json': Permission denied. Die element-config.json auf dem Host ist für den Container nicht lesbar. Das passiert zuverlässig auf Servern mit umask 027, weil neue Dateien dann mit 0640 und ohne Leserecht für andere entstehen. chmod 0644 /opt/matrix/element-config.json und neu starten. Das gilt für jede Konfigurationsdatei, die du per Bind-Mount in einen Container reichst.
Element hängt bei „Confirm your digital identity", und „Use recovery key" ist ausgegraut. Du meldest dich auf einem weiteren Gerät an und hast beim ersten keinen Wiederherstellungsschlüssel angelegt (Schritt 5). Ist ein altes Gerät noch angemeldet, nimm „Use another device" und bestätige dort. Sonst bleibt nur „Can’t confirm?" mit anschließendem Kontopasswort: Element setzt die Identität dann zurück, Konto, Kontakte und Räume bleiben, aber Nachrichten aus der Zeit davor bleiben unlesbar. Leg danach sofort den Wiederherstellungsschlüssel an.
Die Anmeldung schlägt plötzlich mit M_LIMIT_EXCEEDED fehl. In der Antwort steht retry_after_ms, die Wartezeit in Millisekunden. Synapse bremst Anmeldeversuche pro Konto und pro IP ab; das trifft dich, wenn du mehrere Sitzungen hintereinander aufbaust oder mit Skripten testest. Für den Dauerbetrieb lässt sich das über den rc_login-Block in der homeserver.yaml lockern, nur solltest du das nicht tun, denn genau diese Bremse hält Rateversuche auf.
In der Geräteliste und in den Protokollen steht 172.x.x.x statt der echten Besucher-IP. Das betrifft nur Anfragen über IPv6, bei einer Domain mit AAAA-Eintrag also die Mehrheit. Das proxy-Netz wird im Traefik-Tutorial mit docker network create proxy angelegt, und das ist ein reines IPv4-Netz. Docker kann eine eingehende IPv6-Verbindung dann nicht direkt auf den Container umschreiben und schickt sie über einen Hilfsprozess, der die Absenderadresse durch die des Bridge-Gateways ersetzt; über IPv4 stimmt sie die ganze Zeit. Abhilfe ist ein Netz mit IPv6: in allen Stacks docker compose down, dann docker network rm proxy && docker network create --ipv6 --subnet fd00:cafe::/64 proxy und alles wieder hochfahren; die Schritte stehen im Reverse Proxy mit Traefik. Danach steht bei IPv4 und IPv6 die echte Adresse in X-Forwarded-For.
Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →
Wartung & Backups
Element und Synapse aktualisierst du getrennt. Element Web ist eine Sammlung statischer Dateien ohne eigenen Zustand; ein Update ist risikoarm und kostet Sekunden:
cd /opt/matrix
sed -i 's/element-web:v1.12.33/element-web:vNEUE_VERSION/' compose.yaml
docker compose pull element && docker compose up -d element
curl -s https://DEINE_DOMAIN/versionDer letzte Aufruf gibt die laufende Version aus, etwa 1.12.33. Element folgt Synapse locker;
ein Versionssprung des Servers macht die Oberfläche nicht kaputt. Umgekehrt gilt das ebenfalls,
solange du nicht mehrere Jahre überspringst.
Was zusätzlich ins Backup muss, über Teil 1 hinaus:
/opt/matrix/element-config.json # steckt in keiner DatenbankDas ist eine kleine Datei, aber sie enthält jede Entscheidung aus Schritt 2. Geht sie verloren, startet Element mit der Standardkonfiguration, und deine Leute stehen plötzlich vor einem Eingabefeld, in das sie irgendeinen Homeserver tippen können.
Registrierungstokens brauchen Hygiene. Ein Token mit uses_allowed: 3 ohne Ablaufdatum ist ein
Einladungscode, der jahrelang gültig bleibt. Schau alle paar Monate in die Liste und lösch, was du
nicht mehr brauchst:
curl -s -X DELETE "https://DEINE_DOMAIN/_synapse/admin/v1/registration_tokens/DER_TOKEN" \
-H "Authorization: Bearer DEIN_TOKEN"Der Speicherbedarf ist die eigentliche Wartungsaufgabe. Der Sprung von rund 120 auf 280 MB
für einen einzigen föderierten Raum skaliert weiter. Setz dir eine Erinnerung, alle paar Monate
docker stats --no-stream und du -sh /opt/matrix/data/media_store anzuschauen, und tritt aus
Räumen aus, in denen niemand von euch mehr schreibt. Das ist wirksamer als jede Feineinstellung.
Realistisch gerechnet macht Element selbst keine Arbeit: zwei, drei Updates im Jahr. Die Zeit geht in den Homeserver: alle zwei Wochen ein Synapse-Update mit Blick in die Release Notes, einmal im Quartal Medien und Räume aufräumen. Zusammen etwa eine Stunde im Monat.
Feedback per E-Mail: feedback@serverkueche.de
Das könnte dir auch schmecken


