Umlaute in der Oberflaeche, Sitzungskopplung, Statusanzeigen

Echte Umlaute in allen Texten der Oberflaeche und in den Markdown-Dateien.
Die Einbindung von Dateifreigaben haengt jetzt an der Sitzung: wird sie
widerrufen oder laeuft sie ab, verschwinden die Freigaben mit ihr. Neu
einstellbar ist die automatische Abmeldung bei Leerlauf (Vorgabe 30 Minuten).
Ein Geraet, das nicht antwortet, bietet nur noch "Einschalten (PoE)" an --
Neustart ueber SSH oder RPC braucht ein erreichbares Geraet. Der Zeitgeber der
naechsten Pruefung und die Restlaufzeit der Lizenz stehen jetzt oben in der
Kopfzeile; der doppelte Knopf darunter ist weg. Millisekunden werden ab einer
Sekunde als Sekunden und ab einer Minute als m:ss dargestellt.

Ausserdem: Release-Tarball nicht mehr im Repository -- er haengt am Release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 23:54:33 +02:00
co-authored by Claude Opus 5
parent 9a760274f8
commit fbbf8eb87b
68 changed files with 1235 additions and 691 deletions
+56 -56
View File
@@ -1,6 +1,6 @@
# Sicherheit
Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind.
Was geschützt wird, womit, und wo bewusst Grenzen gezogen sind.
---
@@ -9,35 +9,35 @@ Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind.
| | |
|---|---|
| Speicherung | Argon2id (argon2-cffi) |
| Altbestand | bcrypt wird noch geprueft und bei der naechsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt |
| Zweiter Faktor | TOTP, optional erzwingbar fuer Administratoren |
| Altbestand | bcrypt wird noch geprüft und bei der nächsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt |
| Zweiter Faktor | TOTP, optional erzwingbar für Administratoren |
| Sperre je Konto | nach `LOGIN_MAX_ATTEMPTS` Versuchen |
| Sperre je Adresse | zusaetzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst laesst sich die Kontosperre umgehen, indem man viele Konten je einmal probiert |
| Verzeichnisdienst | LDAP nur ueber TLS, Zertifikat wird geprueft. Ohne gueltiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet |
| Sperre je Adresse | zusätzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst lässt sich die Kontosperre umgehen, indem man viele Konten je einmal probiert |
| Verzeichnisdienst | LDAP nur über TLS, Zertifikat wird geprüft. Ohne gültiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet |
Nach einer Anmeldung wird die Sitzungskennung neu vergeben (gegen Session
Fixation) und das CSRF-Token gewechselt.
### Erneute Bestaetigung ("sudo-Modus")
### Erneute Bestätigung ("sudo-Modus")
Alles, was Geheimnisse offenlegt oder Daten aus dem System heraustraegt,
Alles, was Geheimnisse offenlegt oder Daten aus dem System herausträgt,
verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten,
Ausfuhr, Herunterladen eines Lizenzbundles, Schluesselrotation,
2FA-Aenderungen, Deaktivierung einer Lizenz.
Ausfuhr, Herunterladen eines Lizenzbundles, Schlüsselrotation,
2FA-Änderungen, Deaktivierung einer Lizenz.
Ein uebernommenes Sitzungscookie allein reicht dafuer nicht.
Ein übernommenes Sitzungscookie allein reicht dafür nicht.
---
## 2. Sitzungen
Serverseitig in SQLite, nicht im Cookie. Das Cookie traegt nur eine
Serverseitig in SQLite, nicht im Cookie. Das Cookie trägt nur eine
Zufallskennung.
* `HttpOnly`, `SameSite=Lax`, `Secure` sobald HTTPS eingerichtet ist
* Leerlaufzeit **und** absolute Hoechstdauer
* Der Cookiename enthaelt den `SITE_KEY` -- zwei Installationen auf einem Host
koennen sich die Sitzung nicht gegenseitig ueberschreiben (Cookies
* Leerlaufzeit **und** absolute Höchstdauer
* Der Cookiename enthält den `SITE_KEY` -- zwei Installationen auf einem Host
können sich die Sitzung nicht gegenseitig überschreiben (Cookies
unterscheiden keine Ports)
* Aktive Sitzungen sind einsehbar und einzeln beendbar
@@ -45,22 +45,22 @@ Zufallskennung.
## 3. CSRF
Drei voneinander unabhaengige Schichten:
Drei voneinander unabhängige Schichten:
1. `SameSite=Lax` am Sitzungscookie.
2. Ein Token je Sitzung, in jedem Formular, in konstanter Zeit verglichen.
3. Origin-/Referer-Pruefung gegen den Host der Anfrage.
3. Origin-/Referer-Prüfung gegen den Host der Anfrage.
Die dritte Schicht akzeptiert den Host mit und ohne Port sowie die
Standardports -- **nicht** aber beliebige Ports: ein anderer Dienst auf
demselben Rechner ist ein anderer Ursprung.
Der Vorgaenger hatte gar keinen CSRF-Schutz. Eine praeparierte Seite konnte im
Namen eines angemeldeten Administrators Geraete loeschen oder Benutzer anlegen.
Der Vorgänger hatte gar keinen CSRF-Schutz. Eine präparierte Seite konnte im
Namen eines angemeldeten Administrators Geräte löschen oder Benutzer anlegen.
Ausnahmen (`@csrf.exempt`) gibt es nur fuer Maschinenschnittstellen, die sich
**nicht** ueber Cookies autorisieren. Eine cookie-autorisierte Route ohne
CSRF-Schutz waere eine Luecke.
Ausnahmen (`@csrf.exempt`) gibt es nur für Maschinenschnittstellen, die sich
**nicht** über Cookies autorisieren. Eine cookie-autorisierte Route ohne
CSRF-Schutz wäre eine Lücke.
---
@@ -69,75 +69,75 @@ CSRF-Schutz waere eine Luecke.
Bei jeder Antwort:
* `Content-Security-Policy` mit einem Nonce je Anfrage. **Kein**
`unsafe-inline` fuer Skripte -- deshalb gibt es im ganzen Projekt kein
Inline-Script ohne Nonce, und JavaScript haengt ueber
`unsafe-inline` für Skripte -- deshalb gibt es im ganzen Projekt kein
Inline-Script ohne Nonce, und JavaScript hängt über
`data-behavior`-Attribute am DOM. `style-src` erlaubt `unsafe-inline`, weil
einzelne berechnete Breiten (Fortschrittsbalken) als Style-Attribut gesetzt
werden; Style-Attribute fuehren keinen Code aus.
werden; Style-Attribute führen keinen Code aus.
* `X-Content-Type-Options: nosniff`
* `X-Frame-Options: DENY`, `frame-ancestors 'none'`
* `Referrer-Policy: same-origin`
* `Permissions-Policy` -- Kamera, Mikrofon, Ort, Zahlung, USB abgeschaltet
* `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`
* `Strict-Transport-Security` nur, wenn TLS tatsaechlich terminiert wird
* `Strict-Transport-Security` nur, wenn TLS tatsächlich terminiert wird
> HSTS gehoert **nicht** zu einem selbstsignierten Zertifikat. Der Browser
> merkt sich die Vorgabe monatelang; die Warnung laesst sich danach nicht mehr
> HSTS gehört **nicht** zu einem selbstsignierten Zertifikat. Der Browser
> merkt sich die Vorgabe monatelang; die Warnung lässt sich danach nicht mehr
> wegklicken. `install.sh` setzt HSTS deshalb nur zusammen mit Let's Encrypt.
---
## 5. Geheimnisse im Ruhezustand
Ein Schluesselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600):
Ein Schlüsselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600):
* Versionierte Schluessel, rotierbar. Chiffrate tragen ihre Schluesselkennung
* Versionierte Schlüssel, rotierbar. Chiffrate tragen ihre Schlüsselkennung
(`v1.<id>.<nonce>.<ct>`) und bleiben nach einer Rotation lesbar.
* Der Verwendungszweck geht als AAD in die Verschluesselung ein. Ein Chiffrat
aus einem Kontext laesst sich in einem anderen nicht entschluesseln.
* Beim Start wird geprueft, ob die Rechte auf Schluessel- und Lizenzdateien
* Der Verwendungszweck geht als AAD in die Verschlüsselung ein. Ein Chiffrat
aus einem Kontext lässt sich in einem anderen nicht entschlüsseln.
* Beim Start wird geprüft, ob die Rechte auf Schlüssel- und Lizenzdateien
noch stimmen; Abweichungen erscheinen unter *Diagnose*.
Passwoerter fuer Mounts gehen ueber `stdin` an den privilegierten Helfer, der
Passwörter für Mounts gehen über `stdin` an den privilegierten Helfer, der
sie in eine 0600-Datei schreibt -- nie als Kommandozeilenargument (Prozessliste)
und nie ueber die Umgebung (`/proc/<pid>/environ`).
und nie über die Umgebung (`/proc/<pid>/environ`).
---
## 6. Rechtetrennung auf dem Host
Der Webprozess laeuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch
Der Webprozess läuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch
genau einen allowlisted Helfer (`/usr/local/lib/tesm/tesm-helper`), der per
sudoers ausschliesslich fuer diesen Pfad freigegeben ist.
sudoers ausschliesslich für diesen Pfad freigegeben ist.
Regeln, die im Repository durch Tests erzwungen werden:
* Jedes Verb, das die Anwendung kennt, existiert im Helfer.
* Kein `eval`, kein `bash -c`, kein `sh -c`.
* Jede Pfadpruefung weist `..` ab -- sonst laesst sich eine Positivliste ueber
* Jede Pfadprüfung weist `..` ab -- sonst lässt sich eine Positivliste über
den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat
den zulaessigen Basisnamen `x`).
den zulässigen Basisnamen `x`).
* Der Instanzname ist auf `tesm[-<instanz>]` begrenzt; er landet in Pfaden.
* Keine sudoers-Regel ausser der einen. `NOPASSWD: ALL` gibt es nicht.
Die systemd-Units haben `NoNewPrivileges=yes`, `ProtectSystem=strict`,
`PrivateTmp=yes` und eine enge `RestrictAddressFamilies`-Liste.
Der Vorgaenger liess die komplette Flask-Anwendung als root laufen. Eine
einzige Luecke in irgendeinem Pfad haette den Host bedeutet.
Der Vorgänger liess die komplette Flask-Anwendung als root laufen. Eine
einzige Lücke in irgendeinem Pfad hätte den Host bedeutet.
---
## 7. Aenderungsprotokoll
## 7. Änderungsprotokoll
Verkettet gehasht: jeder Eintrag enthaelt den Hash seines Vorgaengers. Ein
nachtraegliches Aendern oder Loeschen faellt auf und wird unter *Diagnose*
Verkettet gehasht: jeder Eintrag enthält den Hash seines Vorgängers. Ein
nachträgliches Ändern oder Löschen fällt auf und wird unter *Diagnose*
angezeigt, mitsamt dem Eintrag, ab dem die Kette bricht.
Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdruecklich
Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdrücklich
auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`).
Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
Ältere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
---
@@ -145,20 +145,20 @@ Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
Siehe [ARCHITEKTUR.md](ARCHITEKTUR.md), Abschnitt Lizenzierung. Kurz:
getrennte Signaturkontexte, kanonisches JSON, Nonce gegen Wiedereinspielung,
Frischefenster, `seq` gegen Rueckstufung, Antwort an die Anfrage gebunden,
Frischefenster, `seq` gegen Rückstufung, Antwort an die Anfrage gebunden,
Aussteller-Pinning.
Das Auslieferungsbundle enthaelt den privaten Clientschluessel und ist damit
Das Auslieferungsbundle enthält den privaten Clientschlüssel und ist damit
selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische
Passwortbestaetigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
Aenderungsprotokoll.
Passwortbestätigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
Änderungsprotokoll.
---
## 9. Ein- und Ausfuhr
Verschluesselte Umschlaege (Argon2id-Ableitung + AES-256-GCM) mit einem selbst
gewaehlten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben
Verschlüsselte Umschläge (Argon2id-Ableitung + AES-256-GCM) mit einem selbst
gewählten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben
wird; die Vorschau liegt in der Datenbank, nicht im Prozessspeicher -- deshalb
braucht der Dienst kein `--workers 1` mehr.
@@ -166,11 +166,11 @@ braucht der Dienst kein `--workers 1` mehr.
## 10. Bewusst nicht getan
* **Kein Schalter, der die LDAP-Zertifikatspruefung abschaltet.** Wer ihn
* **Kein Schalter, der die LDAP-Zertifikatsprüfung abschaltet.** Wer ihn
einmal setzt, setzt ihn dauerhaft.
* **Kein CSRF-Freibrief fuer cookie-autorisierte Routen.**
* **Kein `unsafe-inline` fuer Skripte**, auch nicht "voruebergehend".
* **Keine Rechteausweitung**, auch nicht fuer Administratoren untereinander.
* **Kein CSRF-Freibrief für cookie-autorisierte Routen.**
* **Kein `unsafe-inline` für Skripte**, auch nicht "vorübergehend".
* **Keine Rechteausweitung**, auch nicht für Administratoren untereinander.
* **Kein Hostname im Lizenz-Fingerabdruck** -- Umbenennen darf die Bindung
nicht brechen.
* **Keine von Hand gepflegte nginx-Site.** Sie wird immer neu erzeugt.
@@ -179,5 +179,5 @@ braucht der Dienst kein `--workers 1` mehr.
## 11. Wenn Sie eine Schwachstelle finden
Bitte nicht oeffentlich melden. Wenden Sie sich an die im Anbieterprofil des
Bitte nicht öffentlich melden. Wenden Sie sich an die im Anbieterprofil des
Lizenzservers hinterlegte Adresse.