munin/BETRIEB.md

347 lines
14 KiB
Markdown

# Betriebshandbuch
Was tun, wenn etwas schiefgeht? Diese Seite ist für den Ernstfall geschrieben,
auch für jemanden, der Munin nicht selbst gebaut hat. Jeder Abschnitt beginnt
mit dem, was man sieht, und endet mit dem, was man tut.
Die Befehle gehen vom Docker-Aufbau aus dem README aus: Container `munin`,
Volume unter `/data`. Ohne Docker gilt dasselbe mit dem eigenen `DATA_DIR`
statt `/data`, und die Ausgabe des Prozesses statt `docker logs`.
---
## Auf einen Blick
| Was | Wo |
|---|---|
| Kontroll-Datenbank | `DATA_DIR/control.db`: Workspaces, welcher Key zu welchem Workspace gehört, Workspace-Admins |
| Ein Workspace | `DATA_DIR/workspaces/<workspace-id>.db`: Projekte, Einträge, Keys, Protokoll dieses Workspace |
| Zugehörige Hilfsdateien | `…db-wal` und `…db-shm` neben jeder Datenbank. Sie gehören im laufenden Betrieb dazu, **nie einzeln löschen, solange der Server läuft** |
| Logs | Standardausgabe des Prozesses: `docker logs munin` |
| Sicherungen | `BACKUP_DIR/<zeitstempel>/` von `scripts/backup.sh`, siehe README „Datensicherung“ |
| Markdown-Export | `EXPORT_DIR`, falls gesetzt. Lesbar, aber **keine** vollständige Sicherung (keine Keys, keine Historie) |
| Geheimnisse | `SESSION_SECRET`, `ADMIN_TOKEN` als Umgebungsvariablen, siehe README „Konfiguration“ |
| Lebenszeichen | `GET /healthz` antwortet `ok` |
Welcher Workspace hinter einer ID steckt:
```sh
sqlite3 /data/control.db "SELECT id, name FROM workspaces;"
```
---
## Läuft er?
```sh
curl -s https://<server>/healthz # erwartet: ok
docker logs --tail 50 munin
```
Beim Start schreibt Munin diese Zeilen ins Log. Fehlen sie, ist der Start
gescheitert:
```
Munin läuft auf http://localhost:8080 (3 Workspaces)
Workspaces: /data/workspaces (Kontrolle: /data/control.db)
Schreibbudget: 1000 Aufrufe je Projekt und Stunde (Vorgabe)
```
`/healthz` prüft nur, ob der Prozess antwortet, nicht die Datenbanken. Ein
`ok` heißt also nicht, dass jeder Workspace lesbar ist. Siehe dazu
[Datenbank beschädigt](#datenbank-beschädigt).
---
## Server startet nicht
**Man sieht:** Der Container beendet sich sofort, `/healthz` antwortet nicht.
`docker logs munin` zeigt die Ursache in der letzten Zeile:
| Meldung | Ursache | Abhilfe |
|---|---|---|
| `SESSION_SECRET ist nicht gesetzt` / `zu kurz` | Umgebungsvariable fehlt oder hat weniger als 32 Zeichen | Aus dem Passwortspeicher setzen. **Nicht** neu erzeugen, wenn es ein altes gibt, sonst sind alle Anmeldungen weg |
| `ADMIN_TOKEN ist nicht gesetzt` / `zu kurz` | dasselbe, mindestens 16 Zeichen | dasselbe |
| `Datenverzeichnis konnte nicht angelegt werden` | Volume fehlt oder gehört nicht dem Benutzer im Container (UID 10001) | `chown -R 10001:10001` auf das Volume, oder richtig einhängen |
| `unable to open database file`, `permission denied` | dasselbe für eine einzelne Datei, oft nach einem Zurückkopieren als root | Besitzer der Datei korrigieren |
| `address already in use` | Ein anderer Prozess belegt den Port | anderen Prozess beenden oder `PORT` ändern |
| `database disk image is malformed` | beschädigte Datenbank | [Datenbank beschädigt](#datenbank-beschädigt) |
---
## Platte voll
**Man sieht:** Lesen geht noch, Schreiben schlägt fehl. Im Log steht
`database or disk is full`, Agenten bekommen Fehler bei `remember`.
SQLite bleibt dabei heil. Ein Schreibvorgang, der nicht mehr passt, wird
vollständig zurückgerollt, halbe Einträge gibt es nicht.
1. Nachsehen, was wächst: `du -sh /data/* /data/workspaces/* <BACKUP_DIR>/* <EXPORT_DIR>`
2. Platz schaffen, in dieser Reihenfolge:
- alte Sicherungen löschen und `RETENTION_DAYS` im Cronjob setzen, falls es fehlt
- Docker-Logs begrenzen (`--log-opt max-size=50m`)
3. **Nicht** die `-wal`- oder `-shm`-Dateien löschen. Darin stehen Änderungen,
die noch nicht in der Hauptdatei sind.
Sobald wieder Platz ist, schreibt Munin ohne Neustart weiter.
---
## Ein Key ist in falsche Hände geraten
Zum Beispiel in ein öffentliches Repository eingecheckt, in einem Chat
gepostet, oder der Laptop ist weg.
1. Mit Admin-Rechten anmelden, ins Projekt wechseln, **Zugänge** → beim
betroffenen Key **Zurückziehen**. Das wirkt sofort, für MCP-Aufrufe und für
Sitzungen in der Weboberfläche. Jede Anfrage prüft den Key neu in der
Datenbank.
2. Unter **Protokoll** in der Spalte *Wer* nach dem Namen des Keys suchen (ein
Filter fehlt bisher, die Suche des Browsers hilft): Was wurde damit getan,
von welchen Adressen? Unbekannte Adressen deuten auf Missbrauch hin.
3. Hat der Key geschrieben, was er nicht sollte: Einträge über ihre
**Historie** auf eine frühere Fassung zurücksetzen, Gelöschtes aus dem
**Papierkorb** holen.
4. Dem Team einen neuen Key ausstellen.
Wer den Key nicht kennt, findet ihn über den Workspace: Der Plattform-Admin
sieht alle Workspaces, ein Workspace-Admin alle Projekte seines Workspace.
---
## Ein Workspace-Admin-Token (`wa_…`) ist in falsche Hände geraten
Ein solches Token öffnet den ganzen Workspace.
1. Als Plattform-Admin **Workspaces** → **Bearbeiten** → Abschnitt
*Workspace-Admins* → **Zurückziehen**. Das wirkt sofort, auch für eine
laufende Sitzung.
2. **Protokoll (alle)** des Workspace prüfen: angelegte oder gelöschte
Projekte, neue Keys. Keys, die in der fraglichen Zeit angelegt wurden,
zurückziehen.
3. Neues Token für die Person ausstellen.
---
## `ADMIN_TOKEN` ist in falsche Hände geraten
Mit dem `ADMIN_TOKEN` kann sich jeder als Plattform-Admin anmelden und kommt
an jeden Workspace.
```sh
openssl rand -hex 24 # neues ADMIN_TOKEN
```
1. Neuen Wert im Passwortspeicher ablegen und in der Konfiguration des
Containers setzen.
2. Container neu starten. **Alle Sitzungen des Plattform-Admins enden damit
sofort**, auch die eines Angreifers, der sich schon angemeldet hat. Jede
Admin-Sitzung trägt einen Fingerabdruck des Tokens, mit dem sie entstanden
ist, und der Server vergleicht ihn bei jeder Anfrage (seit 2026-10-06,
siehe `auth.PlatformSubject`). Teams und Workspace-Admins bleiben
angemeldet, MCP-Agenten merken nichts.
3. [Prüfen, was in der fraglichen Zeit geschah](#was-ist-in-der-fraglichen-zeit-geschehen).
## `SESSION_SECRET` ist in falsche Hände geraten
Mit dem `SESSION_SECRET` kann jeder gültige Anmelde-Cookies selbst ausstellen,
für jeden Zugang. Da der Fingerabdruck des `ADMIN_TOKEN` ebenfalls aus dem
`SESSION_SECRET` berechnet wird, schließt das den Plattform-Admin ein, ohne
dass das Token je bekannt war. Das ist der schwerste Fall.
```sh
openssl rand -hex 32 # neues SESSION_SECRET
openssl rand -hex 24 # neues ADMIN_TOKEN, zur Sicherheit gleich mit
```
1. Beide Werte im Passwortspeicher ersetzen und in der Konfiguration des
Containers setzen.
2. Container neu starten. Alle sind danach aus der Weboberfläche abgemeldet und
melden sich mit ihrem Key neu an. **MCP-Agenten merken nichts davon**, sie
nutzen ihre Keys direkt, nicht die Cookies.
3. [Prüfen, was in der fraglichen Zeit geschah](#was-ist-in-der-fraglichen-zeit-geschehen).
### Was ist in der fraglichen Zeit geschehen?
- Anmeldungen des Plattform-Admins stehen **nur im Server-Log**, nicht im
Protokoll: `docker logs munin 2>&1 | grep "Anmeldung Plattform-Admin"`.
Unbekannte Adressen sind ein Warnsignal. Ein selbst ausgestelltes Cookie
(geleaktes `SESSION_SECRET`) hinterlässt diese Zeile allerdings nicht.
- In jedem Workspace das **Protokoll (alle)**: neue Projekte, Keys und
Workspace-Admins. Die Liste der Workspaces auf unbekannte Einträge prüfen.
Was nicht von dir stammt, zurückziehen oder löschen.
---
## Ein Agent dreht durch
**Man sieht:** Ein Projekt füllt sich mit Unsinn, oder Agenten melden, dass das
Schreibbudget erschöpft ist.
Das Schreibbudget (Vorgabe 1.000 Aufrufe je Projekt und Stunde) begrenzt den
Schaden bereits. Ein Agent, der in einer Schleife hängt, wird nach spätestens
einer Stunde gebremst.
1. **Nutzung** im Projekt zeigt, welcher Key wie viel geschrieben hat.
2. Den Key des Agenten **zurückziehen** (siehe oben) oder das Team bitten, den
Agenten zu stoppen.
3. Aufräumen über **Historie** und **Papierkorb**.
4. Braucht ein Projekt dauerhaft mehr: Der Plattform-Admin setzt unter
*Projekte → Bearbeiten* eine eigene Obergrenze.
---
## Einträge versehentlich gelöscht oder überschrieben
Fast immer ohne Sicherung lösbar:
- **Überschrieben:** Eintrag öffnen → **Historie** → frühere Fassung
wiederherstellen. Jede Fassung bleibt erhalten.
- **Gelöscht:** **Papierkorb** im Projekt, 30 Tage lang.
- **Endgültig gelöscht oder älter als 30 Tage im Papierkorb:** nur noch aus einer
Sicherung. Für einzelne Einträge reicht oft der Markdown-Export: Datei
heraussuchen, als ZIP packen und über **Import** im Projekt wieder einlesen.
- **Ganzes Projekt gelöscht:** nur aus einer Sicherung, siehe unten.
---
## Aus einer Sicherung wiederherstellen
Der Ablauf ist am 2026-10-06 einmal vollständig durchgespielt worden (Sicherung
im laufenden Betrieb, Zurückkopieren, Start).
### Vorher bedenken
Eine Sicherung setzt **alles** in der zurückgespielten Datei auf ihren Stand
zurück, nicht nur das, was man retten will:
- Einträge und Fassungen nach der Sicherung sind weg.
- Keys, die nach der Sicherung **angelegt** wurden, gelten nicht mehr.
- Keys, die nach der Sicherung **zurückgezogen** wurden, **gelten wieder.**
Nach dem Zurückspielen deshalb unter *Zugänge* nachsehen und erneut
zurückziehen. Das ist der gefährlichste Nebeneffekt.
Wer nur ein paar Einträge braucht, spielt die Sicherung besser in ein
**getrenntes Verzeichnis** zurück, startet dort einen zweiten Server auf einem
anderen Port, exportiert die Einträge und importiert sie im echten Server.
Dann bleibt alles andere unberührt.
### Einen Workspace zurückholen
Die Kontroll-Datenbank bleibt dabei, wie sie ist. Die anderen Workspaces merken
nichts davon.
```sh
# 1. Welche Datei? (ID ↔ Name)
sqlite3 /data/control.db "SELECT id, name FROM workspaces;"
# 2. Sicherung prüfen, bevor man sich auf sie verlässt
sqlite3 <BACKUP_DIR>/<zeitstempel>/workspaces/<id>.db "PRAGMA integrity_check;" # erwartet: ok
# 3. Server stoppen
docker stop munin
# 4. Die Hilfsdateien der alten Fassung entfernen. Nach einem Absturz liegen
# sie noch da, und SQLite würde sonst ihren Inhalt in die zurückgespielte
# Datei übernehmen.
rm -f /data/workspaces/<id>.db-wal /data/workspaces/<id>.db-shm
# 5. Die kaputte oder falsche Datei zur Seite legen, nicht löschen
mv /data/workspaces/<id>.db /data/workspaces/<id>.db.vorher
# 6. Zurückkopieren, Besitzer richtig setzen
cp <BACKUP_DIR>/<zeitstempel>/workspaces/<id>.db /data/workspaces/
chown 10001:10001 /data/workspaces/<id>.db
# 7. Starten und prüfen
docker start munin
curl -s https://<server>/healthz
```
Danach in der Weboberfläche den Workspace öffnen und **zurückgezogene Keys
erneut zurückziehen** (siehe oben). Läuft alles, kann `<id>.db.vorher` weg.
### Alles zurückholen
Wie oben, aber mit `control.db` und **allen** Dateien unter `workspaces/`, und
mit allen `-wal`/`-shm`-Dateien. Zusätzlich zu den Keys gelten dann auch
Workspace-Admins wieder, die nach der Sicherung zurückgezogen wurden.
### Ein gelöschter Workspace
Beim Löschen verschwinden die Datei **und** sein Eintrag in der
Kontroll-Datenbank. Nur die Datei zurückzukopieren reicht deshalb nicht, der
Server kennt den Workspace nicht mehr. Es braucht auch die `control.db` aus
derselben Sicherung, und die setzt alles Zentrale zurück: andere Workspaces,
Workspace-Admins, die Zuordnung neuer Keys. Das ist eine bewusste Entscheidung.
Meist ist der Weg über einen getrennten zweiten Server mit Export und Import
der bessere.
---
## Datenbank beschädigt
**Man sieht:** `database disk image is malformed` im Log, oder ein Workspace
lässt sich nicht öffnen, während andere gehen. Workspace-Dateien werden erst
beim ersten Zugriff geöffnet, deshalb kann der Fehler auch erst Stunden nach
dem Start auftauchen.
```sh
docker stop munin
sqlite3 /data/workspaces/<id>.db "PRAGMA integrity_check;"
```
Alles außer `ok` heißt: [aus der Sicherung zurückholen](#einen-workspace-zurückholen).
Nicht versuchen, die Datei zu reparieren. Mit der letzten guten Sicherung ist
man schneller und sicherer am Ziel.
Häufigste Ursache ist eine Datei, die im laufenden Betrieb mit `cp` statt mit
`scripts/backup.sh` kopiert und dann zurückgespielt wurde.
---
## Eine neue Fassung einspielen
Das Schema der Datenbanken wird beim Start automatisch angepasst
(`internal/store/migrations*`). Der Weg zurück auf eine ältere Fassung ist
danach nicht garantiert. Deshalb:
1. `scripts/backup.sh` von Hand laufen lassen.
2. Neues Abbild bauen, Container ersetzen.
3. Log auf die Startzeilen prüfen, `/healthz`, einmal in der Weboberfläche
suchen und ein MCP-`recall` absetzen.
4. Geht etwas schief: altes Abbild starten **und** die Sicherung aus Schritt 1
zurückspielen.
Ein Neustart beendet laufende Anfragen sauber (bis zu 10 Sekunden). Agenten
verbinden sich danach von selbst neu.
---
## Regelmäßig
| Wann | Was |
|---|---|
| wöchentlich | Läuft die Sicherung? Ein Blick ins Log des Cronjobs und auf den neuesten Ordner in `BACKUP_DIR`. |
| monatlich | Marke *ungenutzt* bei Keys und Workspace-Admins (Übersicht unter *Workspaces*). Vergessene Zugänge zurückziehen. |
| vierteljährlich | **Probe der Sicherung:** die neueste Sicherung in ein leeres Verzeichnis kopieren, `PRAGMA integrity_check` für jede Datei, einen zweiten Server mit `DATA_DIR` darauf und anderem `PORT` starten, einmal anmelden und suchen. Eine Sicherung, die nie zurückgespielt wurde, ist nur eine Hoffnung. |
| vor jedem Update | Sicherung von Hand, siehe oben |
Die Probe der Sicherung ohne Docker, auf einem beliebigen Rechner mit Go und
sqlite3:
```sh
probe=$(mktemp -d)
cp -r <BACKUP_DIR>/<zeitstempel>/. "$probe/"
for f in "$probe"/control.db "$probe"/workspaces/*.db; do
echo "$f: $(sqlite3 "$f" 'PRAGMA integrity_check;')"
done
DATA_DIR="$probe" PORT=18080 SESSION_SECRET=$(openssl rand -hex 32) \
ADMIN_TOKEN=probe-probe-probe-1 go run .
# im Browser: http://localhost:18080, Anmeldung mit probe-probe-probe-1
```
Die Probe darf mit eigenen Geheimnissen laufen. Sie braucht weder die echten
noch erreicht sie den echten Server.