diff --git a/.htaccess b/.htaccess
index fa44a65..685daec 100644
--- a/.htaccess
+++ b/.htaccess
@@ -10,6 +10,24 @@ Options -Indexes
RewriteEngine On
+ # ------------------------------------------------------------------
+ # 0. TLS erzwingen - und zwar VOR jeder Route
+ # ------------------------------------------------------------------
+ # Die Stelle ist der Punkt. Alle Routen darunter tragen [L]; stuende die
+ # Umleitung dahinter, wuerde sie fuer genau die Adressen nie erreicht,
+ # die es betrifft - login.php, index.php, die gesamte API.
+ #
+ # Ergaenzt HSTS, ersetzt es nicht: HSTS wirkt erst, nachdem der Browser
+ # die Anlage einmal ueber TLS gesehen hat. Die erste Anfrage ist genau
+ # die, die abgefangen wuerde, und die faengt diese Regel ab.
+ #
+ # Die zweite Bedingung gilt TLS-terminierenden Zwischenstellen: Dort ist
+ # %{HTTPS} leer, obwohl der Besucher verschluesselt spricht. Ohne sie
+ # liefe die Umleitung im Kreis.
+ RewriteCond %{HTTPS} !=on
+ RewriteCond %{HTTP:X-Forwarded-Proto} !=https
+ RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+
# Bereits umgeschriebene Anfragen nicht erneut anfassen.
RewriteRule ^public/ - [L]
@@ -74,7 +92,11 @@ Options -Indexes
# 5. Alles Uebrige sperren: Anwendungscode, Konfiguration, Skripte
# ------------------------------------------------------------------
RewriteRule ^(config|src|sql|scripts|cli|var|client-dotnet|client-php|mcp)(/|$) - [F,L]
- RewriteRule ^Serverdaten\.txt$ - [F,L]
+ # Auch die Sicherungskopie. deploy.py schliesst beide aus, die Datei
+ # sollte also nie hier liegen - aber die zweite Verteidigungslinie muss
+ # zur ersten passen, sonst deckt sie genau den Fall nicht ab, fuer den es
+ # sie gibt.
+ RewriteRule ^Serverdaten\.txt(\..*)?$ - [F,L]
# ----------------------------------------------------------------------
@@ -85,12 +107,47 @@ Options -Indexes
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
+
+ # HSTS: Nach dem ersten Besuch spricht der Browser diese Adresse nur noch
+ # ueber TLS an - auch dann, wenn jemand einen http-Link unterschiebt. Die
+ # Umleitung weiter unten schuetzt erst ab der zweiten Anfrage; die erste
+ # ist genau die, die abgefangen wird.
+ #
+ # Ohne preload und ohne includeSubDomains waere es die halbe Wirkung; mit
+ # includeSubDomains gilt es fuer jede Unteradresse von mhdf.de. Wer hier
+ # eine Unteradresse ohne TLS betreibt, macht sie damit unerreichbar - das
+ # ist der Preis und der Grund, warum die Zeile bewusst hier steht und
+ # nicht in einer Vorlage.
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
+
+ # ------------------------------------------------------------------
+ # Content-Security-Policy - und was sie hier NICHT leistet
+ # ------------------------------------------------------------------
+ # 'unsafe-inline' steht in script-src, und das ist keine Nachlaessigkeit,
+ # sondern eine Tatsache ueber den Bestand: index.php enthaelt 93 Inline-
+ # Handler (onclick, onsubmit, onchange) und mehrere tausend Zeilen
+ # Inline-Skript. Ohne 'unsafe-inline' funktioniert die Oberflaeche nicht -
+ # mit ihr schuetzt die Richtlinie nicht gegen eingeschleustes Skript.
+ #
+ # Das ehrlich hinzuschreiben ist wichtiger, als es zu verschweigen: Eine
+ # CSP im Header laedt sonst dazu ein, XSS fuer erledigt zu halten. Was
+ # sie hier WIRKLICH leistet:
+ # - kein Skript von fremden Adressen (Exfiltration ueber