wiki:WebDiensteNginxSecurityHeader

Version 1 (modified by Johannes Lode, 13 days ago) (diff)

--

Security-Header in der NGINX-Konfiguration

Das Problem

add_header in NGINX vererbt nicht so, wie man es erwartet. Die Direktiven werden von der uebergeordneten Ebene nur dann geerbt, wenn auf der aktuellen Ebene kein einziges add_header steht.

Ein beliebiges add_header in einem location-Block loescht damit saemtliche Header aus dem server-Block -- stillschweigend, ohne Warnung beim Konfigurationstest. Genau das ist der Grund, warum der Strict-Transport-Security-Header bei Nextcloud fehlte: die empfohlene Nextcloud-Konfiguration setzt in den PHP- und Asset-Locations eigene Header.

Ab NGINX 1.29.3 laesst sich das mit add_header_inherit merge; abstellen. Unsere Version ist aelter, deshalb die Loesung ueber wiederholte Includes.

Vorsicht bei einer spaeteren Umstellung auf merge: die Direktive haengt die Werte der uebergeordneten Ebene an, sie ersetzt sie nicht. Bei gleichnamigen Headern entstehen Duplikate. Der Fehlermodus wechselt damit von "Header fehlt" zu "Header doppelt" -- bei X-Frame-Options verwerfen mehrere Browser den Header dann komplett, bei CSP wird die Schnittmenge beider Policies durchgesetzt.

Aufbau

Drei Ebenen, per Komposition statt per Kopie:

base-headers.inc          ueberall unschaedlich
  |
  +-- public-headers.inc      alles unter vpn21.net, HSTS-Schalter
  |     |
  |     +-- apex-headers.inc        vpn21.net / www.vpn21.net
  |     +-- nextcloud-headers.inc
  |     +-- usvn-headers.inc
  |     +-- shop-headers.inc        geplant
  |
  +-- intern-headers.inc      *.p21.net, kein HSTS

Alle Dateien liegen in conf.d/ und folgen dem Namensschema *.inc. Die Endung ist wichtig: conf.d/*.conf wird von NGINX per Glob automatisch eingebunden, *.inc nicht.

Einbindung

In den server-Block und in jeden location-Block, der eigene add_header-Direktiven hat -- am besten als erste Zeile:

server {
    include conf.d/nextcloud-headers.inc;

    location ~ \.php$ {
        include conf.d/nextcloud-headers.inc;
        add_header Cache-Control "no-store";
        ...
    }
}

Pruefung

Vergessene Bloecke finden:

nginx -T | awk '/^\s*location/{loc=$0; has=0} /add_header/{if(loc && !has){print loc; has=1}}'

Jeden Location-Typ einmal abklopfen -- PHP, CSS/JS, Fonts, statische Dateien, /.well-known/*:

curl -sI https://cloud.vpn21.net/
curl -sI https://cloud.vpn21.net/nextcloud/index.php
curl -sI https://cloud.vpn21.net/nextcloud/core/css/server.css

Beim Pruefen im Browser auf den Cache achten: angezeigt werden die Header der urspruenglichen Antwort. Mit Shift-Reload gegenpruefen.

Die einzelnen Header

Header Ebene Begruendung
X-Content-Type-Options: nosniff base verhindert MIME-Sniffing, z.B. hochgeladenes Bild als Skript
X-Permitted-Cross-Domain-Policies: none base Adobe-Clients, weitgehend historisch
X-Frame-Options: SAMEORIGIN base Clickjacking-Schutz, abgeloest durch CSP frame-ancestors
Referrer-Policy pro Site Wert unterschiedlich, siehe unten
Strict-Transport-Security public / apex noch nicht aktiviert
X-Robots-Tag: noindex, nofollow Nextcloud, USVN niemals global
Content-Security-Policy pro Site nie global, siehe unten
X-XSS-Protection -- deprecated, nicht setzen

X-Frame-Options

"same origin" bedeutet Schema + Host + Port. Daraus folgt:

  • dienstuebergreifendes Einbetten ist blockiert, z.B. VIS auf iobroker.p21.net, das nodered.p21.net einbettet
  • eine HTTP-Seite, die die HTTPS-Seite desselben Hosts einbettet, ist bereits cross-origin -- relevant bei einer schrittweisen TLS-Umstellung der internen Dienste

Derzeit betten die internen Dashboards nur vom gleichen Host ein, deshalb steht SAMEORIGIN in base-headers.inc. Sollte sich das aendern, wandert der Header in die oeffentlichen Dateien und wird intern durch eine CSP mit frame-ancestors ersetzt.

Referrer-Policy

Bewusst nicht in base-headers.inc, weil der sinnvolle Wert je Site abweicht:

  • no-referrer fuer Nextcloud (setzt es selbst), USVN und den Apex
  • strict-origin-when-cross-origin fuer den Shop -- no-referrer bricht unter Umstaenden Zahlungsdienstleister, Affiliate-Rueckverfolgung und Analytics

X-Robots-Tag

Der Hauptgrund gegen eine einzige globale Header-Datei. none ist aequivalent zu noindex, nofollow; auf dem Shop waere das ein Totalschaden -- ohne Fehlermeldung, mit Wochen Verzoegerung sichtbar.

Der Selbstcheck von Nextcloud vergleicht ausserdem gegen den Literal-String noindex, nofollow. Semantisch gleichwertige Schreibweisen wie none erfuellen ihn nicht.

X-XSS-Protection

Nicht setzen. Der Header steuerte den XSS-Filter von Internet Explorer, Chrome, Edge und Safari; Firefox hat ihn nie implementiert, Edge hat den Filter 2018 entfernt, Chrome 2019. Der Filter konnte in eigentlich sicheren Seiten selbst XSS-Luecken erzeugen. OWASP empfiehlt den Wert 0 oder das Weglassen. Gegen XSS hilft CSP.

Der Header steht noch in der Nextcloud-Beispielkonfiguration -- Altlast.

Content-Security-Policy

CSP gibt dem Browser eine Positivliste vor, aus welchen Quellen er Skripte, Stylesheets, Bilder, Schriften, Verbindungen und Frames laden darf. Alles nicht Aufgefuehrte wird verweigert. Das ist die eigentliche Verteidigung gegen Cross-Site-Scripting: eingeschleustes Inline-JavaScript? wird nicht ausgefuehrt, weil Inline-Skripte nicht erlaubt sind.

Fuer legitime Inline-Skripte gibt es Nonces: der Server erzeugt pro Request eine Zufallszahl, schreibt sie in den Header und an jedes eigene <script>. Eingeschleuste Skripte kennen sie nicht.

CSP gehoert niemals in eine globale Datei. Mehrere CSP-Header werden nicht ueberschrieben, sondern alle durchgesetzt -- der Browser bildet die Schnittmenge. Nextcloud erzeugt seine CSP selbst mit Nonces; eine zweite aus NGINX zerlegt die Oberflaeche.

Fuer den Shop spaeter ueber Content-Security-Policy-Report-Only einfuehren: derselbe Header, der Browser blockiert nichts und meldet nur, was er blockiert haette. Einige Wochen auswerten, aufraeumen, dann scharf schalten.

Doppelte Header aus dem Backend

add_header ergaenzt Header des Backends, es ersetzt sie nicht.

  • Nextcloud sendet seine Security-Header selbst aus PHP. In der fastcgi-Konfiguration fastcgi_param modHeadersAvailable true; setzen, dann unterlaesst Nextcloud das.
  • USVN laeuft auf Apache hinter NGINX. Dort proxy_hide_header verwenden. Die Direktive folgt derselben Vererbungsregel wie add_header und gehoert deshalb in dieselbe Include-Datei. Vorher pruefen, was das Backend tatsaechlich sendet: curl -sI http://127.0.0.1:<backend-port>/

HSTS -- offene Entscheidung

Status: nicht aktiviert. Der Schalter steht auskommentiert in public-headers.inc und apex-headers.inc.

Wirkung: der Browser merkt sich fuer max-age Sekunden, dass der Host nur ueber HTTPS angesprochen werden darf. Zertifikatswarnungen sind dann nicht mehr per Klick uebergehbar.

includeSubDomains erweitert die Policy auf die Subdomains dieses Hosts, nicht auf Geschwister-Hosts. Ein Header von cloud.vpn21.net gilt fuer *.cloud.vpn21.net, nicht fuer shop.vpn21.net. Damit alle *.vpn21.net erfasst werden, muss der Header vom Apex vpn21.net kommen.

Unbedenklich, solange die internen Dienste unter einer getrennten Domain liegen. Vor einer Migration der internen Dienste unter vpn21.net neu bewerten -- includeSubDomains wuerde sie dann mit erfassen und HTTP dort abschalten.

Das woechentliche HTTP-Fenster fuer Let's Encrypt ist nicht betroffen: HSTS ist eine Browser-Policy, der ACME-Validator wertet sie nicht aus, HTTP-01-Challenges funktionieren unveraendert.

preload ist eine separate, nur schwer rueckgaengig zu machende Entscheidung und bleibt vorerst aussen vor.

Namensraum p21.net -- offener Punkt

Die Domain p21.net ist inzwischen oeffentlich vergeben und nicht unsere; die interne Nutzung ist knapp 30 Jahre alt und historisch gewachsen. Bisher keine Konflikte. Die Risiken bestehen unabhaengig vom eigenen Setup:

  • DNS-over-HTTPS. Browser loesen zunehmend am lokalen Resolver vorbei auf. Dann landen Anfragen an iobroker.p21.net beim fremden Server -- per Klartext-HTTP, samt Cookies und ggf. Basic-Auth-Credentials. Ohne TLS gibt es auch keine Warnung, die das abfaengt.
  • HSTS-preload des Eigentuempers mit includeSubDomains wuerde die HTTP-Verbindung zu den internen Diensten schlagartig und dauerhaft verweigern lassen.
  • Zertifikate fuer *.p21.net stellt keine oeffentliche CA aus.

Migrationsoptionen

Option Kosten Bewertung
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.
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.
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.
p21.org / p21.site teuer bzw. vergeben kein Vorteil gegenueber den kostenlosen Optionen

Der ICANN-Draft zu .internal empfiehlt selbst die Subdomain einer eigenen Domain als erste Wahl und die reservierte TLD erst danach.