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>
184 lines
7.0 KiB
Markdown
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.
|