# 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...`) 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//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[-]` 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.