Files
pyramid/docs/DASHBOARD_AUTH_HARDENING.md
T
Bernd Steckmeister e97a748b35 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>
2026-07-04 08:43:42 +02:00

7.1 KiB
Raw Blame History

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:

# 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):

  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.