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>
392 lines
14 KiB
Markdown
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.
|