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