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>
334 lines
12 KiB
Markdown
334 lines
12 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`.
|
|
|
|
---
|
|
|
|
## 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.
|