Files
tesm-license/docs/SICHERHEIT.md
T
alientimandClaude Opus 5 f7805a2180 TESM-Lizenzserver 2.0.0 -- Neubau
Vollstaendiger Neubau der Anwendung. Der vorherige Stand bleibt unveraendert
im Zweig SONNET5 erhalten.

Aufbau: apps/tesm-license (Anwendung), packages/tesm-core (gemeinsamer Kern),
packages/tesm-licensing (Lizenzprotokoll), deploy (Installation, systemd,
privilegierter Helfer), docs, tests. Die verwaltete Anwendung liegt in ihrem
eigenen Repository; beide Repositorien bringen die gemeinsamen Pakete mit,
damit sich jedes allein installieren laesst.

Die wichtigsten Unterschiede zum Vorgaenger:

* Keine doppelte licensing.py -- ein Paket, das beide Anwendungen
  installieren, statt zweier Dateien, die byte-identisch bleiben sollen.
* 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.
* Keine Lizenz ohne master_endpoint: eine Ausstellung ohne Endpunkt wird
  abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann.
* Offline-Aktivierung in beide Richtungen; die Lizenz bleibt als
  "Aktivierung offen" markiert, bis sie zurueckkommt.
* Getrennte Signaturkontexte je Nachrichtenart, Nonce gegen Wiedereinspielung,
  seq gegen das Zurueckrollen auf eine aeltere Lizenz.
* Kein Hostname im Maschinen-Fingerabdruck.
* Verschachtelte Datenbankverbindungen sind ein Fehler, kein Deadlock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:01:23 +02:00

184 lines
7.0 KiB
Markdown

# Sicherheit
Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind.
---
## 1. Anmeldung
| | |
|---|---|
| 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 |
| 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 |
Nach einer Anmeldung wird die Sitzungskennung neu vergeben (gegen Session
Fixation) und das CSRF-Token gewechselt.
### Erneute Bestaetigung ("sudo-Modus")
Alles, was Geheimnisse offenlegt oder Daten aus dem System heraustraegt,
verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten,
Ausfuhr, Herunterladen eines Lizenzbundles, Schluesselrotation,
2FA-Aenderungen, Deaktivierung einer Lizenz.
Ein uebernommenes Sitzungscookie allein reicht dafuer nicht.
---
## 2. Sitzungen
Serverseitig in SQLite, nicht im Cookie. Das Cookie traegt 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
unterscheiden keine Ports)
* Aktive Sitzungen sind einsehbar und einzeln beendbar
---
## 3. CSRF
Drei voneinander unabhaengige 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.
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.
Ausnahmen (`@csrf.exempt`) gibt es nur fuer Maschinenschnittstellen, die sich
**nicht** ueber Cookies autorisieren. Eine cookie-autorisierte Route ohne
CSRF-Schutz waere eine Luecke.
---
## 4. Header
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
`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.
* `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
> HSTS gehoert **nicht** zu einem selbstsignierten Zertifikat. Der Browser
> merkt sich die Vorgabe monatelang; die Warnung laesst 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):
* Versionierte Schluessel, rotierbar. Chiffrate tragen ihre Schluesselkennung
(`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
noch stimmen; Abweichungen erscheinen unter *Diagnose*.
Passwoerter fuer Mounts gehen ueber `stdin` an den privilegierten Helfer, der
sie in eine 0600-Datei schreibt -- nie als Kommandozeilenargument (Prozessliste)
und nie ueber die Umgebung (`/proc/<pid>/environ`).
---
## 6. Rechtetrennung auf dem Host
Der Webprozess laeuft 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.
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
den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat
den zulaessigen 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.
---
## 7. Aenderungsprotokoll
Verkettet gehasht: jeder Eintrag enthaelt den Hash seines Vorgaengers. Ein
nachtraegliches Aendern oder Loeschen faellt 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
auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`).
Aeltere 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 Rueckstufung, Antwort an die Anfrage gebunden,
Aussteller-Pinning.
Das Auslieferungsbundle enthaelt den privaten Clientschluessel und ist damit
selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische
Passwortbestaetigung, `Cache-Control: no-store`, Eintrag im Zustell- und im
Aenderungsprotokoll.
---
## 9. Ein- und Ausfuhr
Verschluesselte Umschlaege (Argon2id-Ableitung + AES-256-GCM) mit einem selbst
gewaehlten 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-Zertifikatspruefung 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 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 oeffentlich melden. Wenden Sie sich an die im Anbieterprofil des
Lizenzservers hinterlegte Adresse.