# 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//` | | Instanzdaten (Datenbank, Schlüssel, Lizenz) | `/srv//data/` | | Protokolle | `/var/log//` | | Umgebungsdatei | `/etc/tesm/.env` | | Zertifikate (selbstsigniert) | `/etc/tesm/certs//` | | nginx-Site | `/etc/nginx/sites-available/` | | systemd-Unit | `/etc/systemd/system/.service` | | Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` | | Verwaltungswerkzeug | `/usr/local/bin/-admin` | `` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusätzlichen Instanz, `tesm-`. Dieser Name (der **SITE_KEY**) benennt alles, was ausserhalb von `/srv/` 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 ` | Öffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. | | `--acme-email ` | 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 ` | 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/-backup-` -- 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/-failed-` 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 `-.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/-upgrade-/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/.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. --- ## 4a. Anmeldebanner (MOTD) `install.sh` legt `/etc/update-motd.d/50-` an. Bei jeder SSH-Anmeldung steht dann der Zustand der Installation im Login-Banner: ``` TESM · TimEShepManager UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · BETRIEBSSYSTEM GEBEN (PXE) 3 Clients online · 0 nicht erreichbar · DHCP: 2 Leases aktiv ``` Das Skript liegt unter `/usr/local/lib/tesm/tesm-motd` und benutzt **nur die Standardbibliothek**: kein venv, kein Import der Anwendung, die Datenbank wird nur lesend geöffnet. Ein Lauf dauert Millisekunden. Der Vorgänger importierte dafür die ganze Flask-Anwendung -- bei jeder Anmeldung, und nach einem Wechsel des Quellverzeichnisses zeigte er stillschweigend die Zahlen einer Datenbank, die niemand mehr füllte. Von Hand prüfen: ```bash sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-opus ``` Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein hängender oder fehlschlagender Bannerlauf darf eine Anmeldung nie blockieren. Ein Banner der Vorgängerfassung (`50-tesm`, das `motd_banner.py` aufruft) wird entfernt -- nur dieses, nichts anderes in dem Verzeichnis. --- ## 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//data` und `/var/log/`. --- ## 6. Root-Rechte Die Anwendung hat keine. Alles, was Root braucht, geht durch **einen** Pfad: ``` /usr/local/lib/tesm/tesm-helper [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- /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.