Das Banner der Vorgaengerfassung hing an /srv/tesm/motd_banner.py und
importierte dafuer die ganze Flask-Anwendung samt deren venv. Nach dem
Umstieg zeigte es damit die Zahlen einer Datenbank, die niemand mehr fuellt --
eingefroren, aber unverdaechtig. Aufgefallen ist es erst, als der alte
Ueberwachungsdienst abgeschaltet wurde.
deploy/tesm-motd loest das anders: **nur Standardbibliothek**, kein venv, kein
Import der Anwendung, die Datenbank wird nur lesend geoeffnet (mode=ro). Ein
Lauf dauert Millisekunden statt eines Flask-Starts. install.sh legt das Skript
nach /usr/local/lib/tesm/ und schreibt /etc/update-motd.d/50-<name>, abgesichert
mit "timeout 3" und "|| true" -- ein Banner darf eine Anmeldung nie blockieren.
Ein Banner der Vorgaengerfassung wird entfernt, aber nur, wenn es genau auf
motd_banner.py zeigt.
Damit es nicht zwei Wahrheiten ueber "online" gibt, steht die Zaehlung der
Statusuebersicht jetzt als Konstante DASHBOARD_COUNT_SQL in monitor.py; das
Banner traegt sie wortgleich, und ein Hygienetest vergleicht beide. Dasselbe
fuer die Zeile unter der Wortmarke. Ein weiterer Test verbietet dem Banner
jeden Import ausserhalb der Standardbibliothek -- genau dieser Import war die
Ursache.
Der Lizenzserver bekommt sein eigenes Banner (Kunden und ausgestellte
Lizenzen). Eine Tagline hat er bewusst nicht: die Funktionsliste von TESM waere
dort falsch, und eine erfundene waere schlimmer als keine.
Version 2.0.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Der Lauf gegen die echte Installation hat zwei Fehler gezeigt, die den
Aktualisierungsweg fuer jede Version unbrauchbar machten:
* Die Suche nach install.sh im entpackten Paket lief mit "-maxdepth 2".
release.sh packt mit dem Praefix "<name>/", das Skript liegt also drei
Ebenen tief -- gefunden wurde nie etwas, und die Meldung schob es auf den
Download ("Im Paket fehlt deploy/install.sh").
* Die CRLF-Pruefung rief "file" auf. Das Paket gehoert nicht zur
Grundinstallation eines Servers; fehlt es, endete die Pipeline in "command
not found", grep bekam nichts zu sehen, der if-Zweig wurde nicht betreten --
und genau der Fehler, den die Pruefung abfangen soll, ging unbemerkt durch.
Der Wagenruecklauf wird jetzt ohne Zusatzpaket gesucht, mit "-U": unter Linux
ohne Wirkung, auf einer Windows-Buildmaschine entscheidend, weil grep dort
den Wagenruecklauf beim Lesen entfernt.
Beide Pruefungen fuehren die echten Zeilen aus dem Skript aus -- die eine
gegen den Verzeichnisaufbau, den release.sh erzeugt, die andere gegen zwei
Dateien mit und ohne Wagenruecklauf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler, die erst beim Aktualisieren auffallen -- also genau dann, wenn
niemand sie sehen will:
* update.sh zeigte auf "gitea.example.invalid" und den Besitzer "wis". Auf
keinem Host konnte das funktionieren, und die Doku erwaehnte die Variablen
nicht, mit denen man es haette umlenken koennen. Die Vorgaben zeigen jetzt
dorthin, wo die Releases liegen; das Repository heisst wie die Anwendung.
* update.sh gab nichts an install.sh weiter. Eine benannte Instanz war damit
nicht aktualisierbar: ohne --instance entsteht neben "tesm-opus" eine
zweite Installation namens "tesm", mit eigener Datenbank und eigener
nginx-Site.
* install.sh setzte bei einer Instanz die Ports auf 8080/8443 zurueck, auch
wenn sie seit der Installation auf Port 80 lief. Ein Reverse Proxy davor
haette danach ins Leere gezeigt. Bestehende Ports werden jetzt aus
Umgebungsdatei und nginx-Site uebernommen -- dieselbe Regel wie beim
Secure-Flag: eine Aktualisierung nimmt nichts weg, was nicht ausdruecklich
neu gesetzt wird.
Zwei Tests fuehren dazu die echten Zeilen aus den Skripten aus, statt sie
nachzubauen. Beide fallen ohne die Korrektur durch.
Ausserdem: die vier subprocess-Aufrufe im Textmodus nageln die Kodierung auf
UTF-8 fest. "text=True" allein nimmt die Locale des Prozesses -- unter systemd
haeufig C/ASCII --, und das Lesen bricht ab, sobald ein Werkzeug einen Umlaut
ausgibt. Der Fehler entstand im Leser-Thread von subprocess und damit weit weg
von seiner Ursache; im Testlauf war er als Warnung sichtbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Fehler, alle drei gemessen statt vermutet.
1. Freigaben wurden nie eingebunden ("mount error(1): Operation not
permitted"). Ursache war NICHT der Mount-Namensraum, sondern
SystemCallFilter=@system-service: darin fehlen mount/umount2, und
SystemCallErrorNumber=EPERM macht daraus genau diese Meldung. Nachgewiesen
in einem nachgebauten Sandkasten: mit "@mount" gelingt die Einbindung,
ohne nicht. Die Unit von TESM erlaubt jetzt "@system-service @mount"; alles
andere an der Haertung bleibt. Der zuvor eingebaute Umweg ueber nsenter ist
zurueckgenommen -- RestrictNamespaces=yes verbietet setns, er konnte nie
funktionieren.
Eingebunden wird damit im Namensraum des Webprozesses. Deshalb ist die
Aufraeumaufgabe fuer abgelaufene Einbindungen aus dem Ueberwachungsdienst in
den Webprozess gewandert: ein anderer Dienst sieht diese Einbindungen nicht
und haette Datenbankzeilen als geloest markiert, waehrend die Freigabe
eingebunden blieb.
2. apt-Installationen aus der Oberflaeche scheiterten mit "dpkg returned an
error code (1)". Der Sandkasten wird an dpkg und dessen Maintainer-Skripte
weitervererbt; mit ProtectKernelLogs=yes liefert "dmesg" ein EPERM und ein
Postinst-Skript bricht daran ab. apt laeuft jetzt ueber systemd-run, also
von PID 1 gestartet und damit ohne unsere Haertung.
3. Der RPC-Neustart loeste SSH aus. run_reboot entschied den Weg anhand von
device["category"] -- und dieser Schluessel bedeutet je Herkunft etwas
anderes: die Wartungsabfrage legt die Kategorie der *Zugangsdaten* darunter,
inventory.get_device die des *Geraets*. Ueber die Statusuebersicht kam ein
Windows-Geraet mit leerer Geraetekategorie an, wurde fuer Linux gehalten und
ueber SSH auf Port 22 angesprochen, bis das Zeitlimit griff. Der Weg wird
jetzt uebergeben -- entschieden wird er ohnehin schon vorher in
restart.resolve. Ausserdem zeigte die Neustart-Historie alles ausser "ssh"
als "PoE-Port"; RPC hat jetzt einen eigenen Eintrag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eine Freigabe wurde nie eingebunden -- "mount error(1): Operation not
permitted". Gemessen: mit denselben Optionen und Zugangsdaten gelingt die
Einbindung aus einer normalen Root-Shell sofort. Ursache ist der eigene
Mount-Namensraum des Dienstes (ProtectSystem, ProtectHome, PrivateTmp,
ReadWritePaths); darin laesst sich kein neues Dateisystem einbinden.
Nachgebaut und belegt: derselbe Aufruf scheitert in einem transienten Unit mit
dieser Haertung und gelingt, sobald er ueber nsenter im Namensraum des Wirts
laeuft. Von dort wird der Mount in den Namensraum des Dienstes weitergegeben,
die Anwendung sieht ihn also -- geprueft, inklusive Lesbarkeit fuer den
Dienstbenutzer.
mount-cifs und umount laufen deshalb ueber nsenter. Ohne nsenter im System
bleibt das Verhalten unveraendert. Die Haertung des Dienstes bleibt vollstaendig
erhalten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entfernt: Tests, Architekturdokumentation und die Serverhaelfte des
Lizenzprotokolls. Beides liegt im Entwicklungsrepository alientim/TESM-DEV.
Das Lizenzprotokoll ist geteilt. TESM braucht nur die gemeinsame Haelfte
(tesm-licensing): Lizenzen verifizieren, Status bewerten, Anfragen stellen,
Antworten pruefen. Ausstellen, erneuern, Antworten signieren und
Schluesselerzeugung liegen jetzt in tesm-licensing-server und damit
ausschliesslich beim Lizenzserver -- ein Client soll den Code zum Ausstellen
nicht einmal mitbringen. Nachgeprueft: kein Modul von TESM oder tesm-core
importiert eine der verschobenen Funktionen.
install.sh installiert entsprechend je Anwendung nur die noetigen Pakete und
bricht mit klarer Meldung ab, wenn die verlangte Anwendung nicht im Baum liegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert
im Zweig SONNET5 erhalten.
Aufbau: apps/tesm (Anwendung), packages/tesm-core (gemeinsamer Kern),
packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd,
privilegierter Helfer), docs, tests. Der Lizenzserver liegt in seinem eigenen
Repository; beide Repositorien bringen die gemeinsamen Pakete mit, damit sich
jedes allein installieren laesst.
Die wichtigsten Unterschiede zum Vorgaenger, jeweils an der Stelle im Code
kommentiert, an der der Fehler entstanden ist:
* 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.
* Die nginx-Site wird bei jedem Lauf inhaltlich verglichen und erneuert.
* Jede erzeugte Konfiguration wird vor dem Uebernehmen geprueft (nginx, Kea).
* Kein Hostname im Lizenz-Fingerabdruck.
* Zwei Installationen auf einem Host stoeren sich nicht (eigener SITE_KEY).
* Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>