# Scoreboard: Architektur, Bedrohungsmodell und Betrieb

## Schutzbedarf

Die Score-Daten sind unkritisch, aber öffentlich sichtbar. Die Verfügbarkeit
der Hauptwebsite und der Schutz des Relay-Hosts sind wichtiger als die
Bestenliste. Deshalb ist das Scoreboard optional: Fällt die API aus, bleibt das
gesamte Spiel inklusive lokalem Fortschritt benutzbar.

## Vertrauensgrenzen

1. Der Browser ist nicht vertrauenswürdig.
2. Nginx ist die einzige öffentlich erreichbare Schnittstelle.
3. Die Score-API ist nur im internen Netz `kernel_panic_backend` erreichbar.
4. SQLite liegt außerhalb des Webroots unter
   `$COMPOSE_DIR/data/`.
5. Die öffentliche Schatzdatei ist ein getrennt erzeugtes Fake-Artefakt.

## Technische Kontrollen

- gleiche Herkunft für Schreibzugriffe; kein CORS
- nur `GET` und `POST`, höchstens 8 KiB JSON
- zwei Rate-Limiter: Nginx und API-Prozess
- Alias-Normalisierung, Längenlimit und Linkfilter
- exakt 3 oder 10 bekannte Fragen je Kapitel
- eindeutige Lauf-ID und idempotentes Eintragen
- serverseitige Neuberechnung aller Ergebniswerte
- parametrisierte SQLite-Abfragen und Schema-Constraints
- maximal 1.000 gespeicherte Einträge
- Container als UID/GID 10001, `read_only`, `cap_drop: ALL`
- `no-new-privileges`, CPU-, RAM-, PID- und Log-Limits
- internes Docker-Netz ohne Internetroute
- nur `/data` beschreibbar; keine Secrets im Container
- Health-Endpunkt und externer Blackbox-Check
- nächtliches Relay-Restic-Backup umfasst Service und Daten

## Bewusste Grenzen

Eine anonyme Bestenliste ohne Konten kann Spam nur begrenzen, nicht vollständig
verhindern. Clientcode und Fragenbank sind Open Source; das Scoreboard ist daher
kein Prüfungs- oder Identitätsnachweis. Bei Missbrauch kann die API abgeschaltet
werden, ohne das Lernspiel neu auszuliefern.

## Wartung

```bash
# Status
ssh "$HOST" 'cd $COMPOSE_DIR && docker compose ps'

# Health intern
ssh "$HOST" 'docker exec kernel-panic-score-api python -I -c "import urllib.request; print(urllib.request.urlopen(\"http://127.0.0.1:8080/health\").read().decode())"'

# SQLite-Konsistenz
ssh "$HOST" 'docker exec kernel-panic-score-api python -I -c "import sqlite3; c=sqlite3.connect(\"/data/scores.db\"); print(c.execute(\"PRAGMA integrity_check\").fetchone()[0])"'

# Backup-konsistente Kopie erzeugen
ssh "$HOST" 'docker exec kernel-panic-score-api python -I -c "import sqlite3; s=sqlite3.connect(\"/data/scores.db\"); d=sqlite3.connect(\"/data/scores-backup.db\"); s.backup(d); d.close(); s.close()"'
```

Ein Restore wird zuerst in einem temporären Verzeichnis geprüft. Die
Produktionsdatei wird niemals bei laufendem Service blind überschrieben.
