Files
tesm/docs/BETRIEB.md
T
alientimandClaude Opus 5 51ae39dd53 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:02 +02:00

14 KiB

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

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:

sudo -u tesm tesm-admin create-admin

Beim Lizenzserver zusätzlich, in dieser Reihenfolge:

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:

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

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:

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:

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.

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:

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:

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

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:

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:

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.

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.

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.

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.

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

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.