Changes between Initial Version and Version 1 of WebDiensteNginxSecurityHeader


Ignore:
Timestamp:
Oct 24, 2026, 12:07:31 PM (2 weeks ago)
Author:
Johannes Lode
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • WebDiensteNginxSecurityHeader

    v1 v1  
     1= Security-Header in der NGINX-Konfiguration =
     2
     3[[PageOutline]]
     4
     5== Das Problem ==
     6
     7`add_header` in NGINX vererbt nicht so, wie man es erwartet. Die Direktiven
     8werden von der uebergeordneten Ebene '''nur dann''' geerbt, wenn auf der
     9aktuellen Ebene '''kein einziges''' `add_header` steht.
     10
     11Ein beliebiges `add_header` in einem `location`-Block loescht damit
     12saemtliche Header aus dem `server`-Block -- stillschweigend, ohne Warnung
     13beim Konfigurationstest. Genau das ist der Grund, warum der
     14`Strict-Transport-Security`-Header bei Nextcloud fehlte: die empfohlene
     15Nextcloud-Konfiguration setzt in den PHP- und Asset-Locations eigene Header.
     16
     17Ab NGINX 1.29.3 laesst sich das mit `add_header_inherit merge;` abstellen.
     18Unsere Version ist aelter, deshalb die Loesung ueber wiederholte Includes.
     19
     20'''Vorsicht bei einer spaeteren Umstellung auf `merge`:''' die Direktive
     21haengt die Werte der uebergeordneten Ebene an, sie ersetzt sie nicht. Bei
     22gleichnamigen Headern entstehen Duplikate. Der Fehlermodus wechselt damit
     23von "Header fehlt" zu "Header doppelt" -- bei `X-Frame-Options` verwerfen
     24mehrere Browser den Header dann komplett, bei CSP wird die Schnittmenge
     25beider Policies durchgesetzt.
     26
     27== Aufbau ==
     28
     29Drei Ebenen, per Komposition statt per Kopie:
     30
     31{{{
     32base-headers.inc          ueberall unschaedlich
     33  |
     34  +-- public-headers.inc      alles unter vpn21.net, HSTS-Schalter
     35  |     |
     36  |     +-- apex-headers.inc        vpn21.net / www.vpn21.net
     37  |     +-- nextcloud-headers.inc
     38  |     +-- usvn-headers.inc
     39  |     +-- shop-headers.inc        geplant
     40  |
     41  +-- intern-headers.inc      *.p21.net, kein HSTS
     42}}}
     43
     44Alle Dateien liegen in `conf.d/` und folgen dem Namensschema `*.inc`. Die
     45Endung ist wichtig: `conf.d/*.conf` wird von NGINX per Glob automatisch
     46eingebunden, `*.inc` nicht.
     47
     48== Einbindung ==
     49
     50In den `server`-Block '''und''' in jeden `location`-Block, der eigene
     51`add_header`-Direktiven hat -- am besten als erste Zeile:
     52
     53{{{#!nginx
     54server {
     55    include conf.d/nextcloud-headers.inc;
     56
     57    location ~ \.php$ {
     58        include conf.d/nextcloud-headers.inc;
     59        add_header Cache-Control "no-store";
     60        ...
     61    }
     62}
     63}}}
     64
     65== Pruefung ==
     66
     67Vergessene Bloecke finden:
     68
     69{{{#!sh
     70nginx -T | awk '/^\s*location/{loc=$0; has=0} /add_header/{if(loc && !has){print loc; has=1}}'
     71}}}
     72
     73Jeden Location-Typ einmal abklopfen -- PHP, CSS/JS, Fonts, statische
     74Dateien, `/.well-known/*`:
     75
     76{{{#!sh
     77curl -sI https://cloud.vpn21.net/
     78curl -sI https://cloud.vpn21.net/nextcloud/index.php
     79curl -sI https://cloud.vpn21.net/nextcloud/core/css/server.css
     80}}}
     81
     82Beim Pruefen im Browser auf den Cache achten: angezeigt werden die Header
     83der urspruenglichen Antwort. Mit Shift-Reload gegenpruefen.
     84
     85== Die einzelnen Header ==
     86
     87||= Header =||= Ebene =||= Begruendung =||
     88|| `X-Content-Type-Options: nosniff` || base || verhindert MIME-Sniffing, z.B. hochgeladenes Bild als Skript ||
     89|| `X-Permitted-Cross-Domain-Policies: none` || base || Adobe-Clients, weitgehend historisch ||
     90|| `X-Frame-Options: SAMEORIGIN` || base || Clickjacking-Schutz, abgeloest durch CSP frame-ancestors ||
     91|| `Referrer-Policy` || pro Site || Wert unterschiedlich, siehe unten ||
     92|| `Strict-Transport-Security` || public / apex || noch nicht aktiviert ||
     93|| `X-Robots-Tag: noindex, nofollow` || Nextcloud, USVN || '''niemals''' global ||
     94|| `Content-Security-Policy` || pro Site || nie global, siehe unten ||
     95|| `X-XSS-Protection` || -- || deprecated, nicht setzen ||
     96
     97=== X-Frame-Options ===
     98
     99"same origin" bedeutet Schema + Host + Port. Daraus folgt:
     100
     101 * dienstuebergreifendes Einbetten ist blockiert, z.B. VIS auf
     102   `iobroker.p21.net`, das `nodered.p21.net` einbettet
     103 * eine HTTP-Seite, die die HTTPS-Seite desselben Hosts einbettet, ist
     104   bereits cross-origin -- relevant bei einer schrittweisen TLS-Umstellung
     105   der internen Dienste
     106
     107Derzeit betten die internen Dashboards nur vom gleichen Host ein, deshalb
     108steht `SAMEORIGIN` in `base-headers.inc`. Sollte sich das aendern, wandert
     109der Header in die oeffentlichen Dateien und wird intern durch eine CSP mit
     110`frame-ancestors` ersetzt.
     111
     112=== Referrer-Policy ===
     113
     114Bewusst nicht in `base-headers.inc`, weil der sinnvolle Wert je Site
     115abweicht:
     116
     117 * `no-referrer` fuer Nextcloud (setzt es selbst), USVN und den Apex
     118 * `strict-origin-when-cross-origin` fuer den Shop -- `no-referrer` bricht
     119   unter Umstaenden Zahlungsdienstleister, Affiliate-Rueckverfolgung und
     120   Analytics
     121
     122=== X-Robots-Tag ===
     123
     124Der Hauptgrund gegen eine einzige globale Header-Datei. `none` ist
     125aequivalent zu `noindex, nofollow`; auf dem Shop waere das ein
     126Totalschaden -- ohne Fehlermeldung, mit Wochen Verzoegerung sichtbar.
     127
     128Der Selbstcheck von Nextcloud vergleicht ausserdem gegen den Literal-String
     129`noindex, nofollow`. Semantisch gleichwertige Schreibweisen wie `none`
     130erfuellen ihn nicht.
     131
     132=== X-XSS-Protection ===
     133
     134Nicht setzen. Der Header steuerte den XSS-Filter von Internet Explorer,
     135Chrome, Edge und Safari; Firefox hat ihn nie implementiert, Edge hat den
     136Filter 2018 entfernt, Chrome 2019. Der Filter konnte in eigentlich sicheren
     137Seiten selbst XSS-Luecken erzeugen. OWASP empfiehlt den Wert `0` oder das
     138Weglassen. Gegen XSS hilft CSP.
     139
     140Der Header steht noch in der Nextcloud-Beispielkonfiguration -- Altlast.
     141
     142=== Content-Security-Policy ===
     143
     144CSP gibt dem Browser eine Positivliste vor, aus welchen Quellen er Skripte,
     145Stylesheets, Bilder, Schriften, Verbindungen und Frames laden darf. Alles
     146nicht Aufgefuehrte wird verweigert. Das ist die eigentliche Verteidigung
     147gegen Cross-Site-Scripting: eingeschleustes Inline-JavaScript wird nicht
     148ausgefuehrt, weil Inline-Skripte nicht erlaubt sind.
     149
     150Fuer legitime Inline-Skripte gibt es '''Nonces''': der Server erzeugt pro
     151Request eine Zufallszahl, schreibt sie in den Header und an jedes eigene
     152`<script>`. Eingeschleuste Skripte kennen sie nicht.
     153
     154'''CSP gehoert niemals in eine globale Datei.''' Mehrere CSP-Header werden
     155nicht ueberschrieben, sondern alle durchgesetzt -- der Browser bildet die
     156Schnittmenge. Nextcloud erzeugt seine CSP selbst mit Nonces; eine zweite
     157aus NGINX zerlegt die Oberflaeche.
     158
     159Fuer den Shop spaeter ueber `Content-Security-Policy-Report-Only`
     160einfuehren: derselbe Header, der Browser blockiert nichts und meldet nur,
     161was er blockiert haette. Einige Wochen auswerten, aufraeumen, dann scharf
     162schalten.
     163
     164== Doppelte Header aus dem Backend ==
     165
     166`add_header` ergaenzt Header des Backends, es ersetzt sie nicht.
     167
     168 * '''Nextcloud''' sendet seine Security-Header selbst aus PHP. In der
     169   fastcgi-Konfiguration `fastcgi_param modHeadersAvailable true;` setzen,
     170   dann unterlaesst Nextcloud das.
     171 * '''USVN''' laeuft auf Apache hinter NGINX. Dort `proxy_hide_header`
     172   verwenden. Die Direktive folgt derselben Vererbungsregel wie
     173   `add_header` und gehoert deshalb in dieselbe Include-Datei. Vorher
     174   pruefen, was das Backend tatsaechlich sendet:
     175   `curl -sI http://127.0.0.1:<backend-port>/`
     176
     177== HSTS -- offene Entscheidung ==
     178
     179Status: nicht aktiviert. Der Schalter steht auskommentiert in
     180`public-headers.inc` und `apex-headers.inc`.
     181
     182Wirkung: der Browser merkt sich fuer `max-age` Sekunden, dass der Host nur
     183ueber HTTPS angesprochen werden darf. Zertifikatswarnungen sind dann nicht
     184mehr per Klick uebergehbar.
     185
     186`includeSubDomains` erweitert die Policy auf die Subdomains '''dieses'''
     187Hosts, nicht auf Geschwister-Hosts. Ein Header von `cloud.vpn21.net` gilt
     188fuer `*.cloud.vpn21.net`, nicht fuer `shop.vpn21.net`. Damit alle
     189`*.vpn21.net` erfasst werden, muss der Header vom Apex `vpn21.net` kommen.
     190
     191Unbedenklich, solange die internen Dienste unter einer getrennten Domain
     192liegen. '''Vor einer Migration der internen Dienste unter `vpn21.net` neu
     193bewerten''' -- `includeSubDomains` wuerde sie dann mit erfassen und HTTP
     194dort abschalten.
     195
     196Das woechentliche HTTP-Fenster fuer Let's Encrypt ist nicht betroffen: HSTS
     197ist eine Browser-Policy, der ACME-Validator wertet sie nicht aus,
     198HTTP-01-Challenges funktionieren unveraendert.
     199
     200`preload` ist eine separate, nur schwer rueckgaengig zu machende
     201Entscheidung und bleibt vorerst aussen vor.
     202
     203== Namensraum p21.net -- offener Punkt ==
     204
     205Die Domain `p21.net` ist inzwischen oeffentlich vergeben und nicht unsere;
     206die interne Nutzung ist knapp 30 Jahre alt und historisch gewachsen.
     207Bisher keine Konflikte. Die Risiken bestehen unabhaengig vom eigenen Setup:
     208
     209 * '''DNS-over-HTTPS.''' Browser loesen zunehmend am lokalen Resolver
     210   vorbei auf. Dann landen Anfragen an `iobroker.p21.net` beim fremden
     211   Server -- per Klartext-HTTP, samt Cookies und ggf.
     212   Basic-Auth-Credentials. Ohne TLS gibt es auch keine Warnung, die das
     213   abfaengt.
     214 * '''HSTS-preload des Eigentuempers''' mit `includeSubDomains` wuerde die
     215   HTTP-Verbindung zu den internen Diensten schlagartig und dauerhaft
     216   verweigern lassen.
     217 * '''Zertifikate''' fuer `*.p21.net` stellt keine oeffentliche CA aus.
     218
     219=== Migrationsoptionen ===
     220
     221||= Option =||= Kosten =||= Bewertung =||
     222|| `p21.local` || keine || '''ungeeignet.''' `.local` ist per RFC 6762 fuer mDNS reserviert. Avahi, Bonjour und systemd-resolved fangen die Namen ab; auf macOS erreicht eine `.local`-Anfrage den Unicast-Resolver grundsaetzlich nicht. ||
     223|| `p21.internal` || keine || '''brauchbar.''' ICANN hat `.internal` im Juli 2024 dauerhaft von der Delegation ausgenommen, ausdrueck­lich fuer private Nutzung. Kann nie von Dritten registriert werden. Zertifikate weiterhin nur ueber eigene CA. ||
     224|| `p21.vpn21.net` || keine || '''beste Variante bei Bedarf an internem TLS.''' Split-Horizon-DNS, Wildcard-Zertifikat via DNS-01, keine CA-Verteilung auf den Clients. Wechselwirkung mit HSTS beachten, siehe oben. ||
     225|| `p21.org` / `p21.site` || teuer bzw. vergeben || kein Vorteil gegenueber den kostenlosen Optionen ||
     226
     227Der ICANN-Draft zu `.internal` empfiehlt selbst die Subdomain einer eigenen
     228Domain als erste Wahl und die reservierte TLD erst danach.