security: Dashboard-Admin-Routen per Interims-Gate auf Heimnetz begrenzt (Befund o)
server.py (auf dem Pi, nicht im Repo): /api/ban, /api/unban, /api/toggle-registration, /api/create-invite, /api/run-stats nur noch fuer Heimnetz-/localhost-Socket-IPs ohne Cloudflare-Header; sonst 403 mit Erklaerung. Headless verifiziert (9 Pruefungen). Bernds A/B-Entscheidung fuer den Fernzugriff bleibt offen (docs/DASHBOARD_AUTH_HARDENING.md). Zusatzbefund: Repo privat schalten wuerde In-App-Updater brechen (ROADMAP). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1243,3 +1243,11 @@
|
|||||||
2026-07-04 08:42 [Edit] /home/steggi/pyramid/PROGRESS.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/PROGRESS.md
|
||||||
2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md
|
2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||||
|
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||||
|
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||||
|
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||||
|
|||||||
+59
-1
@@ -13,6 +13,61 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 – Dashboard-Befund (o): akutes Loch mit aussperr-sicherem Interims-Gate geschlossen
|
||||||
|
|
||||||
|
**Erledigt:** Der KRITISCHE Befund (o) (Dashboard-Admin-Endpoints ohne Auth,
|
||||||
|
öffentlich erreichbar) ist entschärft, OHNE Bernds A/B-Entscheidung
|
||||||
|
vorwegzunehmen und OHNE ihn auszusperren. Erkenntnis, die das möglich machte:
|
||||||
|
Öffentlicher Verkehr erreicht `server.py` (Port 8080) ausschließlich über den
|
||||||
|
Cloudflare-Tunnel-Container (Absender-IP 172.x + `Cf-*`-Header) – Bernds
|
||||||
|
Heimnetz-Zugriffe kommen dagegen direkt mit `192.168.*`-Socket-IP an. Daher
|
||||||
|
„Weg C" als Interims-Gate in `server.py`: Die 5 Admin-Routen (`/api/ban`,
|
||||||
|
`/api/unban`, `/api/toggle-registration`, `/api/create-invite`, `/api/run-stats`)
|
||||||
|
akzeptieren nur noch Anfragen mit Heimnetz-/localhost-Socket-IP (nicht spoofbar)
|
||||||
|
und ohne Cloudflare-Header; sonst 403 mit deutscher Erklärung inkl.
|
||||||
|
Heimnetz-URL – das Dashboard zeigt `d.error` per `alert()`, Bernd sieht also
|
||||||
|
genau, was zu tun ist. Abgelehnte Versuche landen größenbegrenzt (5-MB-Deckel,
|
||||||
|
Lehre aus Befund (l)) in `security_alerts.log` (nur Fixpfad + Socket-IP, kein
|
||||||
|
attacker-kontrollierter Text → kein HTML-Injection-Risiko in der
|
||||||
|
Dashboard-Anzeige). Dienst-Neustart per kill/`Restart=always`, headless
|
||||||
|
verifiziert (9 Prüfungen): öffentlich ban/toggle → 403, localhost UND
|
||||||
|
192.168.178.71 → 400 „Kein Nutzer angegeben" (Gate passiert, App-Validierung
|
||||||
|
erreicht), gespoofter `Cf-Connecting-Ip` aus dem LAN → 403, `GET /` öffentlich
|
||||||
|
200, `/api/livekit-token` 401 und `/api/e2ee-diagnostics` 403 wie bisher
|
||||||
|
(bleiben bewusst öffentlich, die App braucht sie). Test-Einträge aus dem
|
||||||
|
Alert-Log wieder entfernt. Rückbau jederzeit: `server.py.bak-20260704-dashgate`.
|
||||||
|
Doku/ROADMAP nachgezogen (`docs/DASHBOARD_AUTH_HARDENING.md`: Status, Weg C,
|
||||||
|
Restrisiken).
|
||||||
|
|
||||||
|
**Zusätzlicher Befund (für Bernds Secret-Rotation wichtig):** Das Gitea-Repo
|
||||||
|
einfach privat zu schalten ist KEINE schnelle Lösung für den Secret-Leak (k):
|
||||||
|
Der In-App-Updater (`lib/core/update_checker.dart`) holt Releases **anonym**
|
||||||
|
über die öffentliche Gitea-API – Repo privat = Utas installierte App findet
|
||||||
|
keine Updates mehr, und ein Fix ließe sich nicht mehr per Update verteilen
|
||||||
|
(Henne-Ei). In ROADMAP M0 beim Rotations-Punkt als Warnung vermerkt.
|
||||||
|
|
||||||
|
**Offen/Nächster Schritt:** Bernds Entscheidung bleibt für den FERNZUGRIFF
|
||||||
|
relevant (Weg A empfohlen – schließt auch die weiterhin öffentliche
|
||||||
|
Nutzerlisten-/Präsenz-Ansicht; Restrisiken im Doc: LAN-CSRF, offene Ansicht).
|
||||||
|
Danach ggf. Gate ergänzen/ersetzen. Sonst unverändert: übrige M0-Punkte
|
||||||
|
brauchen PC/Gerät oder Bernd (SQLCipher, Rotation, LiveKit-Client-Umstellung,
|
||||||
|
Härtetest).
|
||||||
|
|
||||||
|
**Stolperfallen:** (1) `stats.html` wird alle 5 Minuten von `stats.sh` (Cron)
|
||||||
|
NEU GENERIERT – Änderungen an der HTML-Datei sind flüchtig, UI-Änderungen
|
||||||
|
müssten in `stats.sh` (deshalb bewusst NUR server-seitig gefixt). (2) Auf dem
|
||||||
|
Pi laufen ZWEI cloudflared-Instanzen: die lokal konfigurierte
|
||||||
|
(`cloudflared.yml`, nur Jellyfin) und eine remote-verwaltete
|
||||||
|
(`cloudflared-pingvin`, Ingress liegt im Cloudflare-Dashboard, lokal nicht
|
||||||
|
einsehbar) – Letztere trägt `dashboard.`/`git.steggi-matrix.work`. Wer nur die
|
||||||
|
lokale YAML liest, hält die Subdomains fälschlich für tot. (3) Der
|
||||||
|
Header-Check allein würde nicht reichen (Header sind von Direktverbindern
|
||||||
|
spoofbar/wegzulassen) – die Socket-IP ist die belastbare Wahrheit, der
|
||||||
|
Cf-Header-Check ist nur die zweite Schranke, falls der Tunnel je im
|
||||||
|
Host-Netz läuft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-04 – Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard
|
## 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 –
|
**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist –
|
||||||
@@ -573,7 +628,10 @@ schließt den Review ab.
|
|||||||
Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in
|
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
|
`docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann
|
||||||
sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
|
sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
|
||||||
Punkt aufgenommen.
|
Punkt aufgenommen. **Update (später am 2026-07-04):** Das akute Loch ist mit
|
||||||
|
einem aussperr-sicheren Interims-Gate geschlossen (Admin-Routen nur noch
|
||||||
|
Heimnetz/localhost, siehe PROGRESS-Eintrag „Dashboard-Befund (o)" oben) –
|
||||||
|
offen bleibt nur noch die Fernzugriffs-Entscheidung A/B.
|
||||||
|
|
||||||
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
|
||||||
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
|
||||||
|
|||||||
+16
-11
@@ -60,17 +60,17 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
|||||||
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
|
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
|
||||||
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
|
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
|
||||||
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
|
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
|
||||||
- [ ] **SICHERHEIT (DRINGEND): Dashboard-Admin-Endpoints haben KEINE Auth und sind
|
- [ ] **SICHERHEIT: Dashboard-Admin-Endpoints – akutes Loch mit Interims-Gate
|
||||||
öffentlich** (Fable-5-Review (o), PROGRESS.md 2026-07-04). `server.py` auf dem Pi
|
geschlossen (2026-07-04), Fernzugriffs-Entscheidung offen** (Fable-5-Review (o)).
|
||||||
ist über `https://dashboard.steggi-matrix.work` aus dem Internet erreichbar, und
|
`server.py` nimmt `/api/ban`, `/api/unban`, `/api/toggle-registration`,
|
||||||
`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`,
|
`/api/create-invite`, `/api/run-stats` jetzt NUR noch aus Heimnetz/localhost an
|
||||||
`/api/run-stats` prüfen keinen Token (per `curl` belegt). Ein Fremder könnte Uta
|
(Socket-IP + Cloudflare-Header-Check); Fremde aus dem Internet bekommen 403.
|
||||||
sperren oder die offene Registrierung am Homeserver aktivieren; der Hostname ist
|
Headless verifiziert (öffentlich 403, Heimnetz ok, App-Endpoints unverändert),
|
||||||
über CT-Logs auffindbar. **Bernd wählt:** Weg A (Cloudflare Access vor die
|
siehe PROGRESS.md 2026-07-04. **Bernds Dashboard läuft im Heimnetz unverändert:
|
||||||
Subdomain – kein Code, empfohlen) oder Weg B (eigener `DASH_TOKEN` +
|
`http://192.168.178.71:8080/stats.html`.** Offen bleibt: Will Bernd die
|
||||||
`stats.html`-Prompt). Fertiger Plan: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach
|
Admin-Buttons auch von unterwegs? Dann Weg A (Cloudflare Access, empfohlen –
|
||||||
sofort umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede
|
schließt auch die weiterhin öffentliche Nutzerlisten-Ansicht) oder Weg B
|
||||||
Auth-Ergänzung Bernds Dashboard bis zur Token-Eingabe lahmlegt.
|
(`DASH_TOKEN`), Plan + Restrisiken in `docs/DASHBOARD_AUTH_HARDENING.md`.
|
||||||
- [x] **LiveKit-Token-Route server-seitig gebaut + verifiziert** (Fable-5-Review (n),
|
- [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`
|
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
|
(whoami-Auth, stdlib-HS256-Minting aus `livekit.yaml`), headless geprüft
|
||||||
@@ -97,6 +97,11 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
|||||||
Client umgestellt und verifiziert (neuer Token 200, alter Admin-Token 403). Der
|
Client umgestellt und verifiziert (neuer Token 200, alter Admin-Token 403). Der
|
||||||
Admin-Token selbst muss trotzdem noch rotiert werden (steht oben, Bernds Sache).
|
Admin-Token selbst muss trotzdem noch rotiert werden (steht oben, Bernds Sache).
|
||||||
- Optional: Git-History-Rewrite (Bernds Entscheidung, Repo ist auch Backup).
|
- Optional: Git-History-Rewrite (Bernds Entscheidung, Repo ist auch Backup).
|
||||||
|
- **ACHTUNG bei „Repo einfach privat schalten":** Der In-App-Updater
|
||||||
|
(`lib/core/update_checker.dart`) holt Releases **anonym** über die öffentliche
|
||||||
|
Gitea-API (`/api/v1/repos/steggi/pyramid/releases`). Repo privat = Utas App
|
||||||
|
findet keine Updates mehr (und ein Fix ließe sich nicht mehr per Update
|
||||||
|
verteilen – Henne-Ei). Erst Updater-Strategie klären, dann Sichtbarkeit ändern.
|
||||||
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
|
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
|
||||||
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
|
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
|
||||||
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
|
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
|
||||||
|
|||||||
@@ -1,13 +1,39 @@
|
|||||||
# Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
|
# Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
|
||||||
|
|
||||||
**Status:** KRITISCHER Befund (Fable-5-Review, 2026-07-04), NICHT umgesetzt –
|
**Status (aktualisiert 2026-07-04): Das akute Loch ist mit einem INTERIMS-GATE
|
||||||
braucht Bernds Entscheidung, weil jede Absicherung sein Browser-Dashboard
|
geschlossen („Weg C", siehe unten)** – die fünf Admin-Endpoints akzeptieren nur
|
||||||
verändert (er muss künftig einen Token eingeben oder Cloudflare Access nutzen).
|
noch Anfragen aus dem Heimnetz/localhost; alles, was über den Cloudflare-Tunnel
|
||||||
Bewusst nicht blind auf dem Pi „gefixt", weil ein falscher Griff Bernd aus seinem
|
kommt, bekommt 403 mit einer Erklärung (die das Dashboard per `alert()` anzeigt).
|
||||||
eigenen Admin-Panel aussperren würde (kein Kanal, ihm den neuen Token mitten in
|
Bernds Dashboard funktioniert im Heimnetz unverändert unter
|
||||||
der Nacht mitzuteilen). Analog zu `docs/LIVEKIT_TOKEN_MIGRATION.md`: fertiger,
|
`http://192.168.178.71:8080/stats.html`; die öffentliche URL zeigt weiterhin die
|
||||||
gegen den echten Code geschriebener Plan, damit die Umsetzung nach Bernds Go
|
Ansicht, nur die Admin-Buttons sind dort gesperrt. **Offen bleibt Bernds
|
||||||
sofort starten kann.
|
Entscheidung Weg A (Cloudflare Access, empfohlen) oder Weg B (DASH_TOKEN), falls
|
||||||
|
er die Admin-Buttons auch von unterwegs braucht** – das Gate ersetzt die
|
||||||
|
Entscheidung nicht, es stopft nur das Loch, solange sie aussteht.
|
||||||
|
|
||||||
|
## Interims-Gate („Weg C", aktiv seit 2026-07-04)
|
||||||
|
|
||||||
|
In `server.py` (Konstante `ADMIN_PATHS` + Helfer `lan_denied_reason`): eine
|
||||||
|
Admin-POST-Anfrage wird nur angenommen, wenn (1) **kein** `Cf-Connecting-Ip`/
|
||||||
|
`Cf-Ray`-Header vorhanden ist (= sie kam nicht durch den Cloudflare-Tunnel) und
|
||||||
|
(2) die **Socket-Absender-IP** (nicht spoofbar) `127.0.0.1`/`::1`/`192.168.*`
|
||||||
|
ist. Abgelehnte Versuche landen größenbegrenzt (5 MB-Deckel) in
|
||||||
|
`security_alerts.log` und damit in der Dashboard-Anzeige. Headless verifiziert:
|
||||||
|
öffentliche URL → 403 (ban/toggle), Heimnetz-IP und localhost → App-Validierung
|
||||||
|
erreicht (400 bei leerem Nutzer), gespoofter Cf-Header aus dem LAN → 403,
|
||||||
|
`GET /` öffentlich → 200, `/api/livekit-token` (401 bei ungültigem Token) und
|
||||||
|
`/api/e2ee-diagnostics` (403 bei falschem Token) unverändert öffentlich
|
||||||
|
erreichbar (die App braucht sie, eigene Auth vorhanden).
|
||||||
|
|
||||||
|
**Bekannte Restrisiken des Interims-Gates (Gründe, warum Weg A trotzdem kommen
|
||||||
|
sollte):**
|
||||||
|
- Die **öffentliche Ansicht bleibt offen** und zeigt Nutzerliste + Präsenz
|
||||||
|
(Informations-Preisgabe). Weg A würde auch das schließen.
|
||||||
|
- **CSRF aus dem Heimnetz:** Eine bösartige Webseite in Bernds Browser könnte
|
||||||
|
theoretisch POSTs an `http://192.168.178.71:8080` schicken (moderne
|
||||||
|
Chrome-Versionen blocken Public→Private-Requests, Firefox nicht vollständig).
|
||||||
|
Erst ein Token/Access-Login (Weg A/B) schließt das sauber.
|
||||||
|
- Wer **im Heimnetz** ist, kann weiterhin ohne Anmeldung administrieren.
|
||||||
|
|
||||||
## Problem (belegt, nicht vermutet)
|
## Problem (belegt, nicht vermutet)
|
||||||
|
|
||||||
@@ -132,13 +158,16 @@ zu zeigen; dann als Bernd einloggen und alle Buttons testen. Kein Code-Deploy.
|
|||||||
(Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an
|
(Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an
|
||||||
einem **Test-Nutzer**, nicht an Uta).
|
einem **Test-Nutzer**, nicht an Uta).
|
||||||
|
|
||||||
## Warum jetzt nur der Plan (nicht sofort umgesetzt)
|
## Warum erst nur der Plan kam – und dann doch das Interims-Gate
|
||||||
|
|
||||||
Jede Absicherung macht die Dashboard-Buttons vorübergehend funktionslos, bis
|
Eine Token-/Access-Absicherung (A/B) macht die Dashboard-Buttons überall
|
||||||
Bernd den neuen Token eingibt (Option B) bzw. die Access-App eingerichtet ist
|
funktionslos, bis Bernd aktiv etwas tut (Token eingeben bzw. Access-App
|
||||||
(Option A). Wird das mitten in einer autonomen Nacht-Session deployt, steht Bernd
|
einrichten) – das mitten in einer autonomen Session zu deployen verletzt die
|
||||||
ohne Vorwarnung vor einem toten Admin-Panel und ohne Möglichkeit, spontan
|
CLAUDE.md-Leitplanke „kein Aussperren". Das *Wie* der Anmeldung bleibt deshalb
|
||||||
Spam-Accounts zu bannen – das verletzt die CLAUDE.md-Leitplanke „kein Aussperren".
|
Bernds Richtungsentscheidung. Das später ergänzte Interims-Gate umgeht dieses
|
||||||
Der Befund ist reversibel und nicht destruktiv (er macht nur restriktiver), aber
|
Dilemma: Es braucht keinerlei Aktion von Bernd (Heimnetz-Nutzung läuft
|
||||||
das *Wie* der neuen Anmeldung ist eine echte Richtungsentscheidung → Bernd wählt
|
unverändert weiter, inkl. spontanem Spam-Bann von zu Hause), sperrt aber
|
||||||
A oder B, dann sofortige Umsetzung + headless-Verifikation.
|
Fremde aus dem Internet sofort aus. Einzige Einschränkung: Admin-Buttons über
|
||||||
|
die öffentliche URL gehen bis zur A/B-Entscheidung nicht – die 403-Meldung im
|
||||||
|
Dashboard erklärt das und nennt die Heimnetz-URL. Rückbau = die Gate-Zeilen in
|
||||||
|
`server.py` entfernen (Backup: `server.py.bak-20260704-dashgate`).
|
||||||
|
|||||||
Reference in New Issue
Block a user