security: KRITISCH – Dashboard-Admin-Endpoints ohne Auth (Review-Befund o)

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>
This commit is contained in:
Bernd Steckmeister
2026-07-04 08:43:42 +02:00
parent ba97f942d6
commit e97a748b35
4 changed files with 244 additions and 0 deletions
+7
View File
@@ -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
+75
View File
@@ -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, expnbf =
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
+18
View File
@@ -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:
+144
View File
@@ -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 <DASH_TOKEN>` verlangen:
```python
# 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:
```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 <DASH_TOKEN>` + 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.