Files
tesm/docs/SICHERHEIT.md
T

189 lines
7.3 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 |
| Rechte | Eine Zeile je Funktion; das Administratorkennzeichen gibt den Verwaltungsbereich, nicht alles |
| 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.
Ein echter Administrator darf sich Funktionsrechte über eine Gruppe geben --
das ist gewollt und hinterlässt eine Zeile im Änderungsprotokoll. Was es
nicht gibt, ist der stille Weg: ein Kennzeichen, das ohne Eintrag alles
aufschließt.
* **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.