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:
@@ -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
@@ -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
|
||||
|
||||
+18
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user