Files
tesm-license/docs/BETRIEB.md
T
alientimandClaude Opus 5 2dea198bcc Upgrade von 1.x, Leerlaufabmeldung, Mitschrift im Audit
upgrade.sh -- ein Weg vom Stand SONNET5 auf diese Fassung, mit Daten. Der
Ablauf braucht keine Schalter: alte Installation sichern, abschalten, neu
installieren, exportieren, uebersetzen, einspielen. Die Sicherung steht
bewusst vor dem Export -- ohne --instance installiert install.sh in dasselbe
Verzeichnis, und wer erst exportiert und dann sichert, sichert im
schlechtesten Fall nichts mehr. Gelesen wird nur aus der Kopie.

Dafuer zwei neue Befehle: "legacy-export" liest eine alte Installation
unmittelbar (der Vorgaenger kann nur ueber die Weboberflaeche exportieren --
fuer ein Upgrade der falsche Weg, und unmoeglich, wenn die alte Anwendung nicht
mehr startet) und schreibt eine Datei im alten Format, die deshalb auch als
Rueckweg taugt. "legacy-import" liest eine Datei im neuen Format ein, ueber
transfer.apply_import -- dieselbe Funktion wie die Seite "Sicherung", kein
zweiter Schreibpfad.

Am Lizenzserver hat der Lauf gegen echte Altdaten zwei Fehler gezeigt, die
jede Uebernahme unmoeglich machten:

* Der alte Signaturschluessel ist URL-sicher kodiert (- und _). Das neue
  Protokoll prueft Base64 streng -- richtig so, aber beim Lesen einer alten
  Datei brach die Uebernahme mit einem Traceback ueber "kein gueltiges Base64"
  ab. Umkodiert wird jetzt an der Grenze zum Altformat; der kanonische Decoder
  bleibt streng.
* Der Anbieterblock wurde aus den Einstellungen der *neuen* Installation
  gebaut, und ohne Endpunkt brach die Uebernahme ab. Bei einem Upgrade ist die
  neue Installation frisch -- dort steht nichts. Endpunkt und Anbieterdaten
  kommen jetzt aus der alten Installation, aber nur dort, wo neu nichts steht.
  Der alte Signaturschluessel wird uebernommen: sonst muesste jeder Kunde
  einen Ausstellerwechsel bestaetigen.

Leerlaufabmeldung: sie hat nie ausgeloest. Ein offener Tab fragt seine
Kacheln alle 15 Sekunden neu ab, und jede Abfrage verlaengerte das
Leerlauffenster -- nachweisbar an einer Sitzung, deren last_seen_at eine
Minute alt war, obwohl seit zehn Stunden niemand am Rechner sass. Der Client
kennzeichnet automatische Anfragen jetzt mit einem Kopf, der Server
verlaengert daraufhin nichts, und last_seen_at bleibt, was der Name sagt.
Faellt die Sitzung waehrend des Pollens, antwortet der Server mit 401 statt
mit einer Umleitung: der Tab laedt neu und zeigt die Anmeldemaske, statt
stumm mit alten Zahlen stehenzubleiben.

Im Audit ist der Name der Mitschrift jetzt ein Verweis auf die Datei -- es ist
derselbe Weg wie zuvor, nur einen Klick weit. Verlinkt wird nur, wenn die
Datei existiert und der Betrachter Protokolle lesen darf; der Rest des
Eintrags wird maskiert, denn ein Auditeintrag enthaelt Fremdeingaben.

Ausserdem: Version 2.0.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:43:04 +02:00

392 lines
14 KiB
Markdown

# Betrieb
Dieses Dokument richtet sich an die Person, die die Anlage installiert,
aktualisiert und im Fehlerfall wieder zum Laufen bringt. Es beschreibt, was auf
dem Host tatsächlich passiert -- nicht, wie die Oberfläche bedient wird.
---
## 1. Was wo liegt
| Zweck | Pfad |
|---|---|
| Anwendung (Code, venv, statische Dateien) | `/srv/<name>/` |
| Instanzdaten (Datenbank, Schlüssel, Lizenz) | `/srv/<name>/data/` |
| Protokolle | `/var/log/<name>/` |
| Umgebungsdatei | `/etc/tesm/<name>.env` |
| Zertifikate (selbstsigniert) | `/etc/tesm/certs/<name>/` |
| nginx-Site | `/etc/nginx/sites-available/<name>` |
| systemd-Unit | `/etc/systemd/system/<name>.service` |
| Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` |
| Verwaltungswerkzeug | `/usr/local/bin/<name>-admin` |
`<name>` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusätzlichen
Instanz, `tesm-<instanz>`. Dieser Name (der **SITE_KEY**) benennt alles, was
ausserhalb von `/srv/<name>` liegt. Deshalb können zwei Installationen
derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die
nginx-Site, die netplan-Datei oder das Sitzungscookie zu überschreiben.
Alles, was ein Mensch verlieren würde, liegt unter `data/`. Das Verzeichnis
wird von `install.sh` nie angefasst.
---
## 2. Installation
```bash
sudo bash deploy/install.sh --app tesm
sudo bash deploy/install.sh --app tesm-license
```
Wichtige Optionen:
| Option | Wirkung |
|---|---|
| `--https` | Port 443 mit TLS, Port 80 leitet dorthin um. Ohne `--domain` entsteht ein selbstsigniertes Zertifikat. |
| `--domain <name>` | Öffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. |
| `--acme-email <a>` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestätigen. |
| `--instance <name>` | Zusätzliche Instanz neben der Hauptinstallation. |
| `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur für Testinstanzen sinnvoll). |
| `--skip-packages` | Keine Systempakete installieren. |
Danach:
```bash
sudo -u tesm tesm-admin create-admin
```
Beim Lizenzserver zusätzlich, **in dieser Reihenfolge**:
```bash
sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel -- sofort sichern!
sudo -u tesm-license tesm-license-admin bootstrap # eigene Master-Lizenz
sudo -u tesm-license tesm-license-admin create-admin
```
> **Der Signaturschlüssel ist nicht wiederherstellbar.** Geht
> `data/master_signing_key.json` verloren, ist keine ausgestellte Lizenz mehr
> prüfbar. Sichern Sie ihn getrennt vom Server.
### Zwei Anwendungen auf einem Host
Beide wollen Port 80 und 443. nginx unterscheidet sie am Namen:
```bash
sudo bash deploy/install.sh --app tesm --https
sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.local
```
Die Installation **ohne** Domain wird zur Vorgabe-Site (`default_server`) und
beantwortet alles, was zu keinem Namen passt. Zwei namenlose Sites auf
demselben Port weist nginx ab -- richtig so, und beim Übernehmen sofort
sichtbar.
---
## 3. Aktualisierung
```bash
sudo bash deploy/install.sh --app tesm
```
Derselbe Befehl. Das Skript erkennt an der vorhandenen Datenbank, dass es eine
Aktualisierung ist, und tut dann zusätzlich:
1. **Vollständige Sicherung** nach `/srv/<name>-backup-<zeitstempel>` --
unabhängig davon, ob sich das Schema geändert hat.
2. Dateiabgleich; `data/` bleibt unangetastet.
3. Schema-Migrationen beim Start (versioniert, mit Prüfsumme).
4. **Gesundheitsprüfung.** Antwortet die Anwendung nicht, wird der alte Stand
automatisch zurückgeholt; die fehlgeschlagene Version bleibt unter
`/srv/<name>-failed-<zeitstempel>` zur Analyse liegen.
Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne
`--https`.
### Ohne Quellbaum: `update.sh`
Liegt kein entpacktes Release vor, holt `update.sh` es selbst -- es lädt das
Release-Asset, entpackt es und ruft dessen `install.sh` auf:
```bash
sudo bash /opt/tesm/deploy/update.sh tesm
sudo bash /opt/tesm-license/deploy/update.sh tesm-license
```
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
eine **benannte Instanz** zu aktualisieren:
```bash
sudo bash /opt/tesm-opus/deploy/update.sh tesm latest --instance opus
```
Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit
eigener Datenbank und eigener nginx-Site. Die Ports einer bestehenden
Installation bleiben erhalten, auch wenn sie von den Vorgaben abweichen: eine
Aktualisierung verschiebt nichts, was nicht ausdrücklich neu gesetzt wird.
Ohne Tag wird `latest` **über die API aufgelöst**, nicht in die URL geschrieben:
Gitea liefert unter einem Tag-Namen sonst ein automatisch erzeugtes
Quell-Archiv mit HTTP 200 statt des echten Anhangs -- und das enthält kein
Release, sondern den Repositoriumsstand.
Die Bezugsquelle ist voreingestellt und lässt sich über die Umgebung umlenken,
etwa auf eine Spiegelung:
| Variable | Vorgabe |
|---|---|
| `TESM_RELEASE_BASE` | `https://gitea.int.eertmoed.net` |
| `TESM_RELEASE_OWNER` | `alientim` |
| `TESM_RELEASE_REPO` | der Anwendungsname (`tesm` bzw. `tesm-license`) |
Der erwartete Anhang heisst `<repo>-<tag>.tar.gz`.
### Upgrade vom Stand 1.x (Branch `SONNET5`)
Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte
Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes
Exportformat.
```bash
sudo bash deploy/upgrade.sh --app tesm
sudo bash deploy/upgrade.sh --app tesm-license
```
Der Ablauf ist die Vorgabe und braucht keine Schalter:
| | |
|---|---|
| 1. Sichern | Vollständige Kopie der alten Installation nach `/srv/<name>-upgrade-<zeitstempel>/alt` (ohne `venv`, Verzeichnis 0700) |
| 2. Abschalten | Alter Dienst **und seine Timer** werden gestoppt und deaktiviert |
| 3. Installieren | `install.sh` dieses Verzeichnisses |
| 4. Exportieren | Aus der Kopie entsteht `export-alt.json` im **alten** Format |
| 5. Übersetzen | Daraus `import-neu.json` im neuen Format |
| 6. Einspielen | Diese Datei geht durch denselben Import wie eine Sicherung |
Warum die Sicherung **vor** dem Export steht: ohne `--instance` installiert
`install.sh` in dasselbe Verzeichnis und ersetzt die alte Installation. Wer
erst exportiert und dann sichert, sichert im schlechtesten Fall nichts mehr.
Gelesen wird ausschliesslich aus der Kopie -- die alte Installation wird nie
verändert.
Die beiden Zwischendateien bleiben liegen; ihre Passphrase steht daneben in
`passphrase.txt` (0600, Verzeichnis 0700). `export-alt.json` ist ein echter
Export des Vorgängers und taugt deshalb auch als Rückweg.
**Daneben statt darüber.** Mit `--instance` entsteht eine zweite Installation,
und die alte bleibt vollständig liegen:
```bash
sudo bash deploy/upgrade.sh --app tesm --instance opus --keep-old-running --port 5100 --http-port 8080
```
**Was nicht mitkommt** -- und warum:
* **Lokale Konten.** Der alte Hash ist bcrypt, hier gilt Argon2id. Ein Hash
ist nicht umrechenbar; das ist sein Zweck. Erstes Konto mit
`tesm-admin create-admin`.
* **Gruppen und Rechte.** Die Rechteschlüssel wurden neu geschnitten.
* **LDAP, Webserver, Zertifikat.** Von Hand nachziehen.
Beim **Lizenzserver** gibt es Schritt 4 und 5 nicht: die Übernahme liest die
alte Datenbank unmittelbar, holt Endpunkt und Anbieterdaten aus ihren
Einstellungen und **signiert jede Lizenz neu**. Der alte Signaturschlüssel wird
dabei übernommen -- sonst müsste jeder Kunde einen Ausstellerwechsel
bestätigen; `--new-key` erzwingt das Gegenteil. Der Bericht nennt namentlich,
wer eine neue Lizenzdatei braucht, denn die Bindung an eine Maschine gilt nur
für die alte Datei.
Nur die Anwendung, ohne Daten: `--no-data`. Nur zeigen, was geschähe:
`--dry-run`.
---
## 4. HTTPS
Der Zielzustand ist immer: **Port 443 mit TLS, Port 80 leitet um, ausser dem
ACME-Pfad.**
Drei Wege dorthin:
**a) Bei der Installation** -- siehe oben, `--https`.
**b) Nachträglich über die Kommandozeile:**
```bash
sudo -u tesm tesm-admin web-setup --https --self-signed --apply
sudo -u tesm tesm-admin web-setup --https --domain tesm.firma.de \
--acme --acme-email it@firma.de --hsts --apply
```
`--print-config` zeigt die Datei, ohne etwas zu schreiben.
**c) In der Oberfläche** unter *Einstellungen -> Webserver und TLS*.
Danach muss `COOKIE_SECURE` in `/etc/tesm/<name>.env` auf `1` stehen (alle drei
Wege erledigen das selbst) und der Dienst neu gestartet werden.
### Die Site-Datei wird immer neu erzeugt
Sie ist **kein** Ort für Handarbeit. Jede Übernahme schreibt sie vollständig
aus der Vorlage neu. Was früher von Hand hineingeschrieben wurde -- Domain,
Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der
Anwendung.
Der Grund steht im Vorgängerprojekt: dort wurde die Datei nur angelegt, wenn
sie fehlte. Ein einmal falscher `alias`-Pfad überlebte dadurch jahrelang jedes
Update, und die Oberfläche kam die ganze Zeit ohne Design.
### Selbstsigniert oder Let's Encrypt?
* **Öffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS
sinnvoll.
* **Geschlossenes Netz:** selbstsigniert. Browser zeigen eine Warnung, die
Verbindung ist trotzdem verschlüsselt und das Sitzungscookie darf `Secure`
tragen. **HSTS hier nicht einschalten** -- der Browser merkt sich die Vorgabe
monatelang und die Warnung lässt sich dann nicht mehr wegklicken.
Die Erneuerung von Let's Encrypt läuft über `certbot certonly --webroot`
(bewusst nicht über das nginx-Plugin, das die erzeugte Site umschreiben
würde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar.
---
## 5. Dienste
```bash
systemctl status tesm.service # Weboberfläche (gunicorn)
systemctl status tesm-monitor.service # Überwachungsschleife (nur TESM)
systemctl status tesm-license.service
journalctl -u tesm -n 100 -f
```
Die Units laufen unprivilegiert (`User=tesm`) mit
`NoNewPrivileges`, `ProtectSystem=strict`, `PrivateTmp` und einer engen
`RestrictAddressFamilies`-Liste. Schreibrechte gibt es nur auf `/srv/<name>/data`
und `/var/log/<name>`.
---
## 6. Root-Rechte
Die Anwendung hat keine. Alles, was Root braucht, geht durch **einen** Pfad:
```
/usr/local/lib/tesm/tesm-helper <verb> [argumente]
```
Der Helfer gehört `root`, ist per sudoers ausschliesslich für genau diesen
Pfad mit `NOPASSWD` freigegeben und prüft jedes Argument gegen eine
Positivliste. Er reicht nie etwas an eine Shell weiter (kein `eval`, kein
`sh -c`).
Verben ansehen:
```bash
sudo /usr/local/lib/tesm/tesm-helper
grep -n ')$' /usr/local/lib/tesm/tesm-helper
```
Regel für Änderungen: **Jedes neue Verb braucht eine Argumentprüfung.**
`tests/test_repo_hygiene.py` erzwingt, dass jedes der Anwendung bekannte Verb
im Helfer vorkommt und dass jede Pfadprüfung `..` abweist.
---
## 7. Sicherung und Wiederherstellung
In der Oberfläche unter *Sicherung* -- verschlüsselte Ausfuhr (Argon2id +
AES-256-GCM) mit einem selbst gewählten Kennwort. Der Import zeigt zuerst eine
Vorschau, bevor irgendetwas geschrieben wird.
Auf dem Host reicht:
```bash
sudo systemctl stop tesm
sudo tar -czf /root/tesm-data-$(date +%F).tar.gz -C /srv/tesm data
sudo systemctl start tesm
```
Unverzichtbar:
* `data/app.db` -- alle Daten
* `data/secret.key` -- ohne sie sind alle Sitzungen ungültig
* `data/data.keys` -- ohne sie sind **alle gespeicherten Geheimnisse verloren**
* `data/license.json` und `data/license_key.json`
* Lizenzserver zusätzlich: `data/master_signing_key.json`
---
## 8. Wenn etwas nicht geht
**Die Anwendung antwortet nicht.**
```bash
journalctl -u tesm -n 200 --no-pager
curl -s http://127.0.0.1:5000/gesundheit
```
Antwortet gunicorn, aber der Browser nicht, liegt es an nginx.
**nginx lässt sich nicht neu laden.**
```bash
sudo nginx -t
```
Die Übernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst
zurück. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft
`install.sh`, das eine unbrauchbare Site durch die Vorlage ersetzt.
**Anmeldung schlägt ohne Fehlermeldung fehl.**
Fast immer `COOKIE_SECURE=1` ohne HTTPS. Der Browser bekommt dann ein Cookie,
das er über HTTP nie zurücksendet.
```bash
grep COOKIE_SECURE /etc/tesm/tesm.env
```
**"Die Anfrage kam von einer fremden Herkunft (Origin/Referer)."**
Der Reverse Proxy reicht den Host falsch durch. In der Site-Datei muss
`proxy_set_header Host $http_host;` stehen -- **nicht** `$host`, das verwirft
den Port.
**Statische Dateien fehlen, die Oberfläche ist unformatiert.**
```bash
grep alias /etc/nginx/sites-available/tesm
ls /srv/tesm/static /srv/tesm/static-core
```
Beide `alias`-Pfade müssen in das Installationsverzeichnis zeigen.
**Zwei Instanzen stören sich.**
`SITE_KEY` in beiden `/etc/tesm/*.env` vergleichen -- er muss sich
unterscheiden. Sonst teilen sich beide nginx-Site und Sitzungscookie.
**Die Wartung meldet "Der SSH-Host-Schlüssel ist unbekannt".**
Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden
Schlüssel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf
der Detailseite des Geräts (*Clients -> Gerät -> SSH-Host-Schlüssel*):
erst **Auslesen**, dann den Fingerprint mit einer unabhängigen Quelle
vergleichen, dann **Freigeben**. Die Wartungsübersicht zeigt in einer eigenen
Spalte, für welche Geräte das noch offen ist. Für Switche liegt dieselbe
Freigabe auf der Switch-Detailseite.
Der Schlüssel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM
verbindet sich immer über die hinterlegte IP-Adresse. Ändert sich die IP
eines Geräts, ist der Schlüssel erneut freizugeben.
**Die Prüfkette des Änderungsprotokolls ist gebrochen.**
Unter *Diagnose* sichtbar. Das Protokoll ist verkettet gehasht; ein Bruch heisst,
dass jemand direkt in die Datenbank geschrieben hat. Der Eintrag, ab dem es
nicht mehr stimmt, wird genannt.
---
## 9. Zurück auf eine ältere Version
```bash
sudo systemctl stop tesm
sudo mv /srv/tesm /srv/tesm-kaputt
sudo cp -a /srv/tesm-backup-<zeitstempel> /srv/tesm
sudo systemctl start tesm
```
Schema-Migrationen laufen nur vorwärts. Ein Rücksprung über eine
Schema-Änderung hinweg braucht deshalb auch die Datenbank aus der Sicherung --
genau das liegt im Backup-Verzeichnis.