Fable-5-Review (n)+(o) beim Gegenlesen von server.py auf dem Pi: (o) KRITISCH: /api/ban, /api/unban, /api/toggle-registration, /api/create-invite, /api/run-stats sind über https://dashboard.steggi-matrix.work oeffentlich erreichbar und pruefen KEINEN Token (per curl belegt: App-eigene 400-Antwort ueber die oeffentliche URL). Fremde koennten Uta sperren oder offene Registrierung aktivieren; Hostname ist ueber CT-Logs auffindbar. Nicht blind gefixt (jede Auth-Ergaenzung legt Bernds Dashboard bis zur Token-Eingabe lahm -> kein Aussperren). Fertiger Plan mit zwei Wegen (Cloudflare Access / DASH_TOKEN) in docs/DASHBOARD_AUTH_HARDENING.md, ROADMAP M0 dringend, wartet auf Bernds Wahl. (n) LiveKit-Token-Route POST /api/livekit-token wurde von einer abgebrochenen Session bereits live in server.py gebaut (unprotokolliert). Geprueft + headless verifiziert (401/400/413/200, JWT-Signatur unabhaengig gegen livekit.yaml gueltig, Identitaet = gepruefte user_id). Server-Seite des M0-LiveKit-Punkts erledigt; Client-Umstellung + Rotation bleiben PC/Geraet (heiliger Call-Pfad). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.1 KiB
Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
Status: KRITISCHER Befund (Fable-5-Review, 2026-07-04), NICHT umgesetzt –
braucht Bernds Entscheidung, weil jede Absicherung sein Browser-Dashboard
verändert (er muss künftig einen Token eingeben oder Cloudflare Access nutzen).
Bewusst nicht blind auf dem Pi „gefixt", weil ein falscher Griff Bernd aus seinem
eigenen Admin-Panel aussperren würde (kein Kanal, ihm den neuen Token mitten in
der Nacht mitzuteilen). Analog zu docs/LIVEKIT_TOKEN_MIGRATION.md: fertiger,
gegen den echten Code geschriebener Plan, damit die Umsetzung nach Bernds Go
sofort starten kann.
Problem (belegt, nicht vermutet)
Der Dashboard-Server /home/steggi/matrix/server.py (läuft als
matrix-stats.service auf 0.0.0.0:8080) ist über Cloudflare unter
https://dashboard.steggi-matrix.work öffentlich aus dem Internet erreichbar.
Getestet: GET / liefert 200 (stats.html), und die zustandsändernden POST-Endpoints
antworten mit der App-eigenen Validierung – ohne jede Authentifizierung:
$ curl -X POST https://dashboard.steggi-matrix.work/api/ban -d '{"user":""}'
{"error": "Kein Nutzer angegeben"} # HTTP 400 – App-Antwort, KEIN Login davor
Damit kann jeder Fremde im Internet, der den Hostnamen kennt, ohne Anmeldung:
/api/ban//api/unban– jeden Matrix-Nutzer sperren/entsperren (auch Uta!) → Aussperrung / Denial-of-Service gegen echte Nutzer./api/toggle-registrationund/api/create-invite– offene Registrierung am Homeserver aktivieren (setztallow_registration = true+yes_i_am_very_very_sure…) → der Homeserver wird zur Spam-/Missbrauchsschleuder./api/run-stats– Stats-Skript auslösen (harmlos, aber ebenfalls ungeschützt).
Der Hostname ist nicht wirklich geheim: Cloudflare stellt TLS-Zertifikate aus,
die in den öffentlichen Certificate-Transparency-Logs landen – jede Subdomain von
steggi-matrix.work ist per CT-Log-Scan auffindbar. Die bisherige „Sicherheit"
beruht also allein auf Verborgenheit eines Namens, der praktisch entdeckbar ist.
Bereits korrekt abgesichert (zum Vergleich, nicht betroffen):
/api/e2ee-diagnostics– eigenerDIAG_TOKEN(Bearer)./api/livekit-token– Matrix-Access-Token perwhoamigegen Continuwuity.
Nur die vier Admin-/Stats-Endpoints oben sind offen.
Wie das Dashboard heute aufruft
stats.html ruft alle Aktionen als Same-Origin-fetch ohne Auth-Header auf
(toggleRegistration, createInvite, refreshStats, doUserAction →
/api/ban //api/unban). Es gibt keine Session, kein Cookie, keinen Token –
deshalb bricht jede hinzugefügte Authentifizierung das Dashboard so lange,
bis stats.html den Token mitschickt. Genau darum ist das eine Bernd-Entscheidung
und kein stiller Fix.
Option A (empfohlen, robust): Cloudflare Access vor die Subdomain
Am saubersten wird das Dashboard komplett durch Cloudflare Access geschützt
(Zero-Trust-Login vor der Subdomain), statt in server.py Auth nachzubauen:
- Im Cloudflare-Zero-Trust-Dashboard eine Access-Application für
dashboard.steggi-matrix.workanlegen. - Policy: nur Bernds E-Mail (
bernd.steckmeister@gmail.com) per One-Time-PIN oder Google-Login zulassen. - Fertig –
server.pyundstats.htmlbleiben unverändert, das Dashboard funktioniert für Bernd wie bisher (nach dem Access-Login), aber Fremde kommen gar nicht erst an die Endpoints.
Vorteil: kein Secret im Code, kein Umbau der HTML-Fetches, kein Aussperr-Risiko durch vergessene Tokens. Nachteil: Bernd muss die Access-App einmal im Cloudflare-Panel klicken (kein Code, aber sein Konto nötig). Das ist der Königsweg – Option B nur, falls Access nicht gewünscht ist.
Option B (Fallback, rein in server.py): eigener Dashboard-Token
Wenn Access nicht gewünscht ist, ein neuer, zufälliger DASH_TOKEN (getrennt
vom geleakten Admin-Token J5lax… und vom DIAG_TOKEN!), den die vier
Admin-/Stats-Endpoints als Authorization: Bearer <DASH_TOKEN> verlangen:
# server.py, zu den anderen Token-Konstanten:
DASH_TOKEN = "<neu erzeugen: python3 -c 'import secrets;print(\"dash-\"+secrets.token_urlsafe(24))'>"
def _dash_authed(self):
return self.headers.get("Authorization", "") == "Bearer " + DASH_TOKEN
In do_POST vor der Behandlung von /api/ban, /api/unban,
/api/toggle-registration, /api/create-invite, /api/run-stats einfügen:
if self.path in ("/api/ban", "/api/unban", "/api/toggle-registration",
"/api/create-invite", "/api/run-stats") and not self._dash_authed():
self.send_error(403)
return
GET / und /api/registration-status bleiben öffentlich (nur Lesen), damit die
Stats-Seite ohne Token sichtbar bleibt.
stats.html darf den Token NIEMALS fest eingebettet ausliefern (die Seite
ist öffentlich – der Token läge sonst für jeden offen). Stattdessen einmal im
Browser abfragen und in localStorage halten:
function dashToken() {
var t = localStorage.getItem('dashToken');
if (!t) { t = prompt('Dashboard-Token:'); if (t) localStorage.setItem('dashToken', t); }
return t || '';
}
// und bei JEDEM Aktions-fetch:
fetch('/api/ban', { method:'POST',
headers: {'Content-Type':'application/json', 'Authorization':'Bearer ' + dashToken()},
body: JSON.stringify({user:u}) })
(betrifft toggleRegistration, createInvite, refreshStats, doUserAction).
Bei 403 den gecachten Token verwerfen (localStorage.removeItem('dashToken')) und
neu fragen. Bernd gibt den Token so genau einmal pro Browser ein.
Rollout / Testplan
Option A: Access-App klicken → von einem fremden Netz/Inkognito prüfen, dass
https://dashboard.steggi-matrix.work/ zum Access-Login umleitet statt die Seite
zu zeigen; dann als Bernd einloggen und alle Buttons testen. Kein Code-Deploy.
Option B (headless auf dem Pi verifizierbar):
server.py+stats.htmländern, Dienst-Neustart (Prozess killen,Restart=alwayslädt neu – siehe PROGRESS 2026-07-04 zur polkit-Falle).curl -X POST .../api/ban -d '{"user":"x"}'ohne Bearer → 403 erwartet.- Mit
Authorization: Bearer <DASH_TOKEN>+ leerem Nutzer → 400 (App-Validierung erreicht) → beweist: Token akzeptiert, Auth greift. - Im Browser: Dashboard öffnen, Token eingeben, je ein Button testen (Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an einem Test-Nutzer, nicht an Uta).
Warum jetzt nur der Plan (nicht sofort umgesetzt)
Jede Absicherung macht die Dashboard-Buttons vorübergehend funktionslos, bis Bernd den neuen Token eingibt (Option B) bzw. die Access-App eingerichtet ist (Option A). Wird das mitten in einer autonomen Nacht-Session deployt, steht Bernd ohne Vorwarnung vor einem toten Admin-Panel und ohne Möglichkeit, spontan Spam-Accounts zu bannen – das verletzt die CLAUDE.md-Leitplanke „kein Aussperren". Der Befund ist reversibel und nicht destruktiv (er macht nur restriktiver), aber das Wie der neuen Anmeldung ist eine echte Richtungsentscheidung → Bernd wählt A oder B, dann sofortige Umsetzung + headless-Verifikation.