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>
184 lines
7.0 KiB
Markdown
184 lines
7.0 KiB
Markdown
# Sicherheit
|
|
|
|
Was geschützt wird, womit, und wo bewusst Grenzen gezogen sind.
|
|
|
|
---
|
|
|
|
## 1. Anmeldung
|
|
|
|
| | |
|
|
|---|---|
|
|
| Speicherung | Argon2id (argon2-cffi) |
|
|
| 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 | 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 Bestätigung ("sudo-Modus")
|
|
|
|
Alles, was Geheimnisse offenlegt oder Daten aus dem System herausträgt,
|
|
verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten,
|
|
Ausfuhr, Herunterladen eines Lizenzbundles, Schlüsselrotation,
|
|
2FA-Änderungen, Deaktivierung einer Lizenz.
|
|
|
|
Ein übernommenes Sitzungscookie allein reicht dafür nicht.
|
|
|
|
---
|
|
|
|
## 2. Sitzungen
|
|
|
|
Serverseitig in SQLite, nicht im Cookie. Das Cookie trägt nur eine
|
|
Zufallskennung.
|
|
|
|
* `HttpOnly`, `SameSite=Lax`, `Secure` sobald HTTPS eingerichtet ist
|
|
* 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
|
|
|
|
---
|
|
|
|
## 3. CSRF
|
|
|
|
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-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 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 für Maschinenschnittstellen, die sich
|
|
**nicht** über Cookies autorisieren. Eine cookie-autorisierte Route ohne
|
|
CSRF-Schutz wäre eine Lücke.
|
|
|
|
---
|
|
|
|
## 4. Header
|
|
|
|
Bei jeder Antwort:
|
|
|
|
* `Content-Security-Policy` mit einem Nonce je Anfrage. **Kein**
|
|
`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 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 tatsächlich terminiert wird
|
|
|
|
> 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 Schlüsselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600):
|
|
|
|
* 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 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*.
|
|
|
|
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 über die Umgebung (`/proc/<pid>/environ`).
|
|
|
|
---
|
|
|
|
## 6. Rechtetrennung auf dem Host
|
|
|
|
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 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 Pfadprüfung weist `..` ab -- sonst lässt sich eine Positivliste über
|
|
den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat
|
|
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 Vorgänger liess die komplette Flask-Anwendung als root laufen. Eine
|
|
einzige Lücke in irgendeinem Pfad hätte den Host bedeutet.
|
|
|
|
---
|
|
|
|
## 7. Änderungsprotokoll
|
|
|
|
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 -- ausdrücklich
|
|
auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`).
|
|
|
|
Ältere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen.
|
|
|
|
---
|
|
|
|
## 8. Lizenzprotokoll
|
|
|
|
Siehe [ARCHITEKTUR.md](ARCHITEKTUR.md), Abschnitt Lizenzierung. Kurz:
|
|
getrennte Signaturkontexte, kanonisches JSON, Nonce gegen Wiedereinspielung,
|
|
Frischefenster, `seq` gegen Rückstufung, Antwort an die Anfrage gebunden,
|
|
Aussteller-Pinning.
|
|
|
|
Das Auslieferungsbundle enthält den privaten Clientschlüssel und ist damit
|
|
selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische
|
|
Passwortbestätigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
|
|
Änderungsprotokoll.
|
|
|
|
---
|
|
|
|
## 9. Ein- und Ausfuhr
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## 10. Bewusst nicht getan
|
|
|
|
* **Kein Schalter, der die LDAP-Zertifikatsprüfung abschaltet.** Wer ihn
|
|
einmal setzt, setzt ihn dauerhaft.
|
|
* **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.
|
|
|
|
---
|
|
|
|
## 11. Wenn Sie eine Schwachstelle finden
|
|
|
|
Bitte nicht öffentlich melden. Wenden Sie sich an die im Anbieterprofil des
|
|
Lizenzservers hinterlegte Adresse.
|