Files
tesm-license/docs/BETRIEB.md
T
alientimandClaude Opus 5 f7805a2180 TESM-Lizenzserver 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert
im Zweig SONNET5 erhalten.

Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern),
packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd,
privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem
eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit,
damit sich jedes allein installieren laesst.

Die wichtigsten Unterschiede zum Vorgaenger:

* Keine doppelte licensing.py -- ein Paket, das beide Anwendungen
  installieren, statt zweier Dateien, die byte-identisch bleiben sollen.
* Der Webprozess laeuft unprivilegiert; alles, was Root braucht, geht ueber
  einen einzigen Helfer mit Positivlisten fuer jedes Argument.
* CSRF-Schutz ueberhaupt -- der Vorgaenger hatte keinen.
* Rechte werden serverseitig geprueft, nicht nur im Template ausgeblendet.
* Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird
  abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann.
* Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als
  "Aktivierung offen" markiert, bis sie zurueckkommt.
* Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung,
  seq gegen das Zurueckrollen auf eine aeltere Lizenz.
* Kein Hostname im Maschinen-Fingerabdruck.
* Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:01:23 +02:00

10 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 tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird.


1. Was wo liegt

Zweck Pfad
Anwendung (Code, venv, statische Dateien) /srv/<name>/
Instanzdaten (Datenbank, Schluessel, 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 zusaetzlichen Instanz, tesm-<instanz>. Dieser Name (der SITE_KEY) benennt alles, was ausserhalb von /srv/<name> liegt. Deshalb koennen zwei Installationen derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die nginx-Site, die netplan-Datei oder das Sitzungscookie zu ueberschreiben.

Alles, was ein Mensch verlieren wuerde, 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> Oeffentlicher 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 bestaetigen.
--instance <name> Zusaetzliche Instanz neben der Hauptinstallation.
--port / --http-port / --https-port Abweichende Ports (nur fuer Testinstanzen sinnvoll).
--skip-packages Keine Systempakete installieren.

Danach:

sudo -u tesm tesm-admin create-admin

Beim Lizenzserver zusaetzlich, in dieser Reihenfolge:

sudo -u tesm-license tesm-license-admin init-key      # Signaturschluessel -- sofort sichern!
sudo -u tesm-license tesm-license-admin bootstrap     # eigene Master-Lizenz
sudo -u tesm-license tesm-license-admin create-admin

Der Signaturschluessel ist nicht wiederherstellbar. Geht data/master_signing_key.json verloren, ist keine ausgestellte Lizenz mehr pruefbar. 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 Uebernehmen 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 zusaetzlich:

  1. Vollstaendige Sicherung nach /srv/<name>-backup-<zeitstempel> -- unabhaengig davon, ob sich das Schema geaendert hat.
  2. Dateiabgleich; data/ bleibt unangetastet.
  3. Schema-Migrationen beim Start (versioniert, mit Pruefsumme).
  4. Gesundheitspruefung. Antwortet die Anwendung nicht, wird der alte Stand automatisch zurueckgeholt; die fehlgeschlagene Version bleibt unter /srv/<name>-failed-<zeitstempel> zur Analyse liegen.

Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne --https.


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) Nachtraeglich ueber 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 Oberflaeche 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 fuer Handarbeit. Jede Uebernahme schreibt sie vollstaendig aus der Vorlage neu. Was frueher von Hand hineingeschrieben wurde -- Domain, Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der Anwendung.

Der Grund steht im Vorgaengerprojekt: dort wurde die Datei nur angelegt, wenn sie fehlte. Ein einmal falscher alias-Pfad ueberlebte dadurch jahrelang jedes Update, und die Oberflaeche kam die ganze Zeit ohne Design.

Selbstsigniert oder Let's Encrypt?

  • Oeffentlich erreichbar, echter DNS-Name: Let's Encrypt. Nur dann ist HSTS sinnvoll.
  • Geschlossenes Netz: selbstsigniert. Browser zeigen eine Warnung, die Verbindung ist trotzdem verschluesselt und das Sitzungscookie darf Secure tragen. HSTS hier nicht einschalten -- der Browser merkt sich die Vorgabe monatelang und die Warnung laesst sich dann nicht mehr wegklicken.

Die Erneuerung von Let's Encrypt laeuft ueber certbot certonly --webroot (bewusst nicht ueber das nginx-Plugin, das die erzeugte Site umschreiben wuerde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar.


5. Dienste

systemctl status tesm.service          # Weboberflaeche (gunicorn)
systemctl status tesm-monitor.service  # Ueberwachungsschleife (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 gehoert root, ist per sudoers ausschliesslich fuer genau diesen Pfad mit NOPASSWD freigegeben und prueft 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 fuer Aenderungen: Jedes neue Verb braucht eine Argumentpruefung. tests/test_repo_hygiene.py erzwingt, dass jedes der Anwendung bekannte Verb im Helfer vorkommt und dass jede Pfadpruefung .. abweist.


7. Sicherung und Wiederherstellung

In der Oberflaeche unter Sicherung -- verschluesselte Ausfuhr (Argon2id + AES-256-GCM) mit einem selbst gewaehlten 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 ungueltig
  • data/data.keys -- ohne sie sind alle gespeicherten Geheimnisse verloren
  • data/license.json und data/license_key.json
  • Lizenzserver zusaetzlich: 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 laesst sich nicht neu laden.

sudo nginx -t

Die Uebernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst zurueck. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft install.sh, das eine unbrauchbare Site durch die Vorlage ersetzt.

Anmeldung schlaegt ohne Fehlermeldung fehl. Fast immer COOKIE_SECURE=1 ohne HTTPS. Der Browser bekommt dann ein Cookie, das er ueber HTTP nie zuruecksendet.

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 Oberflaeche ist unformatiert.

grep alias /etc/nginx/sites-available/tesm
ls /srv/tesm/static /srv/tesm/static-core

Beide alias-Pfade muessen in das Installationsverzeichnis zeigen.

Zwei Instanzen stoeren 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-Schluessel ist unbekannt". Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden Schluessel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf der Detailseite des Geraets (Clients -> Geraet -> SSH-Host-Schluessel): erst Auslesen, dann den Fingerprint mit einer unabhaengigen Quelle vergleichen, dann Freigeben. Die Wartungsuebersicht zeigt in einer eigenen Spalte, fuer welche Geraete das noch offen ist. Fuer Switche liegt dieselbe Freigabe auf der Switch-Detailseite.

Der Schluessel wird nach IP und Port gespeichert, nicht nach Name -- TESM verbindet sich immer ueber die hinterlegte IP-Adresse. Aendert sich die IP eines Geraets, ist der Schluessel erneut freizugeben.

Die Pruefkette des Aenderungsprotokolls 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. Zurueck auf eine aeltere 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 vorwaerts. Ein Ruecksprung ueber eine Schema-Aenderung hinweg braucht deshalb auch die Datenbank aus der Sicherung -- genau das liegt im Backup-Verzeichnis.