diff --git a/CHANGES.md b/CHANGES.md index 0594ef5..13a3f08 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1236,3 +1236,10 @@ 2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py 2026-07-04 04:31 [Edit] /home/steggi/pyramid/PROGRESS.md 2026-07-04 04:32 [Edit] /home/steggi/pyramid/PROGRESS.md +2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py +2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py +2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py +2026-07-04 08:42 [Write] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md +2026-07-04 08:42 [Edit] /home/steggi/pyramid/PROGRESS.md +2026-07-04 08:43 [Edit] /home/steggi/pyramid/PROGRESS.md +2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md diff --git a/PROGRESS.md b/PROGRESS.md index d164c6b..aa1fc13 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -13,6 +13,42 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format: --- +## 2026-07-04 – Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard + +**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist – +zwei neue Punkte (n)/(o) im Fable-5-Review-Abschnitt abgehakt (Details dort). +- **(n):** Beim Gegenlesen von `server.py` gefunden, dass die geplante + LiveKit-Token-Route `POST /api/livekit-token` von einer vorher abgebrochenen + (unprotokollierten) Session **bereits live gebaut** wurde. Kritisch geprüft und + headless verifiziert: Auth per whoami greift (ungültig→401), Body-Limit (413), + gültig→200; das JWT unabhängig dekodiert und die **Signatur gegen `livekit.yaml` + gegengerechnet – gültig**; Identität = geprüfte Matrix-user_id (kein Spoofing). + Route ist gefahrlos additiv. Server-Seite des M0-LiveKit-Punkts damit erledigt; + Client-Umstellung + Rotation bleiben PC/Gerät (heiliger Call-Pfad). +- **(o) KRITISCH:** Derselbe Dashboard-Server hat für seine **Admin-Endpoints + (`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`, + `/api/run-stats`) KEINE Authentifizierung** und ist über + `https://dashboard.steggi-matrix.work` **öffentlich erreichbar**. Belegt per + `curl` (App-eigene 400-Antwort über die öffentliche URL, kein Login davor). + Ein Fremder könnte damit Uta (jeden Nutzer) sperren oder die offene + Registrierung am Homeserver aktivieren. Der Hostname ist über CT-Logs auffindbar. + +**Offen/Nächster Schritt:** **Bernd muss entscheiden, WIE das Dashboard künftig +authentifiziert** – Weg A (Cloudflare Access vor die Subdomain, kein Code, kein +Aussperr-Risiko, empfohlen) oder Weg B (eigener `DASH_TOKEN` + `stats.html`-Prompt). +Fertiger Plan mit beiden Wegen: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach sofort +umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede Auth-Ergänzung +Bernds eigenes Admin-Panel bis zur Token-Eingabe lahmlegt („kein Aussperren") – +das mitten in einer autonomen Session zu deployen ist die falsche Reihenfolge. + +**Stolperfallen:** Ein Review, der sich auf den *geänderten* Endpoint (Diagnose, +Disk-Fill) konzentriert, übersieht leicht die *unveränderten Nachbarn* in +derselben Datei. Lehre: bei einem Sicherheits-Review einer Datei nicht nur den +Diff prüfen, sondern die ganze Angriffsfläche der Datei (hier: alle Routen des +`do_POST`-Handlers auf Auth abklopfen, nicht nur die neu berührte). + +--- + ## 2026-07-04 – Fable-5-Review (l)+(m): Diagnose-Endpoint hatte kein Größenlimit (gefixt) **Erledigt:** Review-Pass über die zwei seit (k) dazugekommenen Arbeiten – als @@ -499,6 +535,45 @@ schließt den Review ab. `127.0.0.1:6167` (whoami-Basis). Python-Snippet (stdlib-HS256) fachlich korrekt (base64url ohne Padding, kompaktes JSON, HMAC-SHA256). Anmerkung: Der Plan setzt zusätzlich `nbf`/`name` – LiveKit akzeptiert beides, harmlos. +- [x] **(n) LiveKit-Token-Route ist inzwischen LIVE auf dem Pi gebaut (bisher + unprotokolliert) – geprüft und funktionsfähig, aber Client noch nicht + umgestellt (2026-07-04).** Beim Review von `server.py` gefunden: Eine + Vorsession hat die in `docs/LIVEKIT_TOKEN_MIGRATION.md` geplante Route + `POST /api/livekit-token` bereits in `server.py` implementiert (whoami-Auth, + stdlib-HS256-Minting aus `livekit.yaml`) – der CHANGES-Hook zeigt drei + server.py-Edits um 04:33, Dienst-Neustart 04:34, aber kein PROGRESS-Eintrag + (Session vor dem Protokoll abgebrochen). Headless verifiziert (127.0.0.1:8080 + UND öffentlich): ungültiger Matrix-Token → 401, fehlende Felder → 400, 5-KB-Body + → 413 (4-KB-Limit), gültiger Admin-Token → 200; das geminete JWT unabhängig + dekodiert: iss=LKMatrixPi, sub/name/metadata = geprüfte user_id (kein Spoofing, + Client kann Identität NICHT wählen), video-Grants wie im Client, exp−nbf = + 21600 s, **Signatur unabhängig gegen `livekit.yaml` gegengerechnet: gültig**. + Die Route ist gefahrlos additiv (altes Client-Secret bleibt bis zur Rotation + gültig, nichts bricht). **Offen bleibt:** Client-Umstellung + (`livekit_token.dart`/`livekit_call_manager.dart`) + Secret-Rotation – beides + weiterhin PC/Gerät (heiliger Call-Pfad), siehe Migrationsplan. Server-Seite des + M0-LiveKit-Punkts ist damit erledigt. +- [x] **(o) KRITISCHER Befund beim server.py-Review: die Admin-Endpoints des + Dashboards haben KEINE Authentifizierung und sind öffentlich erreichbar + (2026-07-04).** Der Review von (l) hatte nur das Diagnose-Endpoint auf + Disk-Fill gehärtet – der viel größere Nachbar blieb unbemerkt: + `https://dashboard.steggi-matrix.work` ist über Cloudflare offen aus dem + Internet erreichbar, und `POST /api/ban`, `/api/unban`, `/api/toggle-registration`, + `/api/create-invite`, `/api/run-stats` prüfen **keinen** Token. Belegt: + `curl -X POST .../api/ban -d '{"user":""}'` liefert die App-eigene 400-Antwort + `{"error":"Kein Nutzer angegeben"}` – identisch zu 127.0.0.1, also KEIN + Cloudflare-Login davor. Damit könnte jeder Fremde Uta (jeden Nutzer) sperren + oder die offene Registrierung am Homeserver aktivieren. Der Hostname ist über + Certificate-Transparency-Logs praktisch auffindbar – „geheim" trägt nicht. + **Bewusst NICHT blind gefixt:** Jede Auth-Ergänzung macht Bernds + Browser-Dashboard funktionslos, bis er einen Token einträgt / Cloudflare Access + einrichtet – das mitten in einer autonomen Session zu deployen sperrt ihn ohne + Vorwarnung aus seinem eigenen Admin-Panel („kein Aussperren"). Fertiger, + gegen den echten Code geschriebener Plan mit zwei Wegen (A: Cloudflare Access = + Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in + `docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann + sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender + Punkt aufgenommen. **Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken (verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene diff --git a/ROADMAP.md b/ROADMAP.md index 0eb2a33..822f494 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -60,6 +60,24 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert. - [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden (`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth) +- [ ] **SICHERHEIT (DRINGEND): Dashboard-Admin-Endpoints haben KEINE Auth und sind + öffentlich** (Fable-5-Review (o), PROGRESS.md 2026-07-04). `server.py` auf dem Pi + ist über `https://dashboard.steggi-matrix.work` aus dem Internet erreichbar, und + `/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`, + `/api/run-stats` prüfen keinen Token (per `curl` belegt). Ein Fremder könnte Uta + sperren oder die offene Registrierung am Homeserver aktivieren; der Hostname ist + über CT-Logs auffindbar. **Bernd wählt:** Weg A (Cloudflare Access vor die + Subdomain – kein Code, empfohlen) oder Weg B (eigener `DASH_TOKEN` + + `stats.html`-Prompt). Fertiger Plan: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach + sofort umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede + Auth-Ergänzung Bernds Dashboard bis zur Token-Eingabe lahmlegt. +- [x] **LiveKit-Token-Route server-seitig gebaut + verifiziert** (Fable-5-Review (n), + PROGRESS.md 2026-07-04). `POST /api/livekit-token` läuft live in `server.py` + (whoami-Auth, stdlib-HS256-Minting aus `livekit.yaml`), headless geprüft + (401/400/413/200, JWT-Signatur unabhängig gegen `livekit.yaml` gültig, Identität = + geprüfte user_id). **Offen:** Client-Umstellung (`livekit_token.dart`/ + `livekit_call_manager.dart`) + Secret-Rotation – beides PC/Gerät (heiliger + Call-Pfad), siehe `docs/LIVEKIT_TOKEN_MIGRATION.md`. - [ ] **SICHERHEIT: geleakte Secrets rotieren + aus dem Client holen** (Fable-5-Review (k), PROGRESS.md 2026-07-04). Das Gitea-Repo ist öffentlich (`private:false`), und es lagen vier aktive Secrets drin – teils schon bereinigt (release.ps1, Admin-Doku), aber: diff --git a/docs/DASHBOARD_AUTH_HARDENING.md b/docs/DASHBOARD_AUTH_HARDENING.md new file mode 100644 index 0000000..845e354 --- /dev/null +++ b/docs/DASHBOARD_AUTH_HARDENING.md @@ -0,0 +1,144 @@ +# 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-registration` und `/api/create-invite` – **offene Registrierung am + Homeserver aktivieren** (setzt `allow_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` – eigener `DIAG_TOKEN` (Bearer). +- `/api/livekit-token` – Matrix-Access-Token per `whoami` gegen 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: + +1. Im Cloudflare-Zero-Trust-Dashboard eine **Access-Application** für + `dashboard.steggi-matrix.work` anlegen. +2. Policy: nur Bernds E-Mail (`bernd.steckmeister@gmail.com`) per One-Time-PIN + oder Google-Login zulassen. +3. Fertig – `server.py` und `stats.html` bleiben **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 ` verlangen: + +```python +# server.py, zu den anderen Token-Konstanten: +DASH_TOKEN = "" + +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: + +```python +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: + +```javascript +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):** +1. `server.py` + `stats.html` ändern, Dienst-Neustart (Prozess killen, + `Restart=always` lädt neu – siehe PROGRESS 2026-07-04 zur polkit-Falle). +2. `curl -X POST .../api/ban -d '{"user":"x"}'` **ohne** Bearer → **403** erwartet. +3. Mit `Authorization: Bearer ` + leerem Nutzer → 400 (App-Validierung + erreicht) → beweist: Token akzeptiert, Auth greift. +4. 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.