| | 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 |
| | 8 | werden von der uebergeordneten Ebene '''nur dann''' geerbt, wenn auf der |
| | 9 | aktuellen Ebene '''kein einziges''' `add_header` steht. |
| | 10 | |
| | 11 | Ein beliebiges `add_header` in einem `location`-Block loescht damit |
| | 12 | saemtliche Header aus dem `server`-Block -- stillschweigend, ohne Warnung |
| | 13 | beim Konfigurationstest. Genau das ist der Grund, warum der |
| | 14 | `Strict-Transport-Security`-Header bei Nextcloud fehlte: die empfohlene |
| | 15 | Nextcloud-Konfiguration setzt in den PHP- und Asset-Locations eigene Header. |
| | 16 | |
| | 17 | Ab NGINX 1.29.3 laesst sich das mit `add_header_inherit merge;` abstellen. |
| | 18 | Unsere Version ist aelter, deshalb die Loesung ueber wiederholte Includes. |
| | 19 | |
| | 20 | '''Vorsicht bei einer spaeteren Umstellung auf `merge`:''' die Direktive |
| | 21 | haengt die Werte der uebergeordneten Ebene an, sie ersetzt sie nicht. Bei |
| | 22 | gleichnamigen Headern entstehen Duplikate. Der Fehlermodus wechselt damit |
| | 23 | von "Header fehlt" zu "Header doppelt" -- bei `X-Frame-Options` verwerfen |
| | 24 | mehrere Browser den Header dann komplett, bei CSP wird die Schnittmenge |
| | 25 | beider Policies durchgesetzt. |
| | 26 | |
| | 27 | == Aufbau == |
| | 28 | |
| | 29 | Drei Ebenen, per Komposition statt per Kopie: |
| | 30 | |
| | 31 | {{{ |
| | 32 | base-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 | |
| | 44 | Alle Dateien liegen in `conf.d/` und folgen dem Namensschema `*.inc`. Die |
| | 45 | Endung ist wichtig: `conf.d/*.conf` wird von NGINX per Glob automatisch |
| | 46 | eingebunden, `*.inc` nicht. |
| | 47 | |
| | 48 | == Einbindung == |
| | 49 | |
| | 50 | In den `server`-Block '''und''' in jeden `location`-Block, der eigene |
| | 51 | `add_header`-Direktiven hat -- am besten als erste Zeile: |
| | 52 | |
| | 53 | {{{#!nginx |
| | 54 | server { |
| | 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 | |
| | 67 | Vergessene Bloecke finden: |
| | 68 | |
| | 69 | {{{#!sh |
| | 70 | nginx -T | awk '/^\s*location/{loc=$0; has=0} /add_header/{if(loc && !has){print loc; has=1}}' |
| | 71 | }}} |
| | 72 | |
| | 73 | Jeden Location-Typ einmal abklopfen -- PHP, CSS/JS, Fonts, statische |
| | 74 | Dateien, `/.well-known/*`: |
| | 75 | |
| | 76 | {{{#!sh |
| | 77 | curl -sI https://cloud.vpn21.net/ |
| | 78 | curl -sI https://cloud.vpn21.net/nextcloud/index.php |
| | 79 | curl -sI https://cloud.vpn21.net/nextcloud/core/css/server.css |
| | 80 | }}} |
| | 81 | |
| | 82 | Beim Pruefen im Browser auf den Cache achten: angezeigt werden die Header |
| | 83 | der 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 | |
| | 107 | Derzeit betten die internen Dashboards nur vom gleichen Host ein, deshalb |
| | 108 | steht `SAMEORIGIN` in `base-headers.inc`. Sollte sich das aendern, wandert |
| | 109 | der Header in die oeffentlichen Dateien und wird intern durch eine CSP mit |
| | 110 | `frame-ancestors` ersetzt. |
| | 111 | |
| | 112 | === Referrer-Policy === |
| | 113 | |
| | 114 | Bewusst nicht in `base-headers.inc`, weil der sinnvolle Wert je Site |
| | 115 | abweicht: |
| | 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 | |
| | 124 | Der Hauptgrund gegen eine einzige globale Header-Datei. `none` ist |
| | 125 | aequivalent zu `noindex, nofollow`; auf dem Shop waere das ein |
| | 126 | Totalschaden -- ohne Fehlermeldung, mit Wochen Verzoegerung sichtbar. |
| | 127 | |
| | 128 | Der Selbstcheck von Nextcloud vergleicht ausserdem gegen den Literal-String |
| | 129 | `noindex, nofollow`. Semantisch gleichwertige Schreibweisen wie `none` |
| | 130 | erfuellen ihn nicht. |
| | 131 | |
| | 132 | === X-XSS-Protection === |
| | 133 | |
| | 134 | Nicht setzen. Der Header steuerte den XSS-Filter von Internet Explorer, |
| | 135 | Chrome, Edge und Safari; Firefox hat ihn nie implementiert, Edge hat den |
| | 136 | Filter 2018 entfernt, Chrome 2019. Der Filter konnte in eigentlich sicheren |
| | 137 | Seiten selbst XSS-Luecken erzeugen. OWASP empfiehlt den Wert `0` oder das |
| | 138 | Weglassen. Gegen XSS hilft CSP. |
| | 139 | |
| | 140 | Der Header steht noch in der Nextcloud-Beispielkonfiguration -- Altlast. |
| | 141 | |
| | 142 | === Content-Security-Policy === |
| | 143 | |
| | 144 | CSP gibt dem Browser eine Positivliste vor, aus welchen Quellen er Skripte, |
| | 145 | Stylesheets, Bilder, Schriften, Verbindungen und Frames laden darf. Alles |
| | 146 | nicht Aufgefuehrte wird verweigert. Das ist die eigentliche Verteidigung |
| | 147 | gegen Cross-Site-Scripting: eingeschleustes Inline-JavaScript wird nicht |
| | 148 | ausgefuehrt, weil Inline-Skripte nicht erlaubt sind. |
| | 149 | |
| | 150 | Fuer legitime Inline-Skripte gibt es '''Nonces''': der Server erzeugt pro |
| | 151 | Request 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 |
| | 155 | nicht ueberschrieben, sondern alle durchgesetzt -- der Browser bildet die |
| | 156 | Schnittmenge. Nextcloud erzeugt seine CSP selbst mit Nonces; eine zweite |
| | 157 | aus NGINX zerlegt die Oberflaeche. |
| | 158 | |
| | 159 | Fuer den Shop spaeter ueber `Content-Security-Policy-Report-Only` |
| | 160 | einfuehren: derselbe Header, der Browser blockiert nichts und meldet nur, |
| | 161 | was er blockiert haette. Einige Wochen auswerten, aufraeumen, dann scharf |
| | 162 | schalten. |
| | 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 | |
| | 179 | Status: nicht aktiviert. Der Schalter steht auskommentiert in |
| | 180 | `public-headers.inc` und `apex-headers.inc`. |
| | 181 | |
| | 182 | Wirkung: der Browser merkt sich fuer `max-age` Sekunden, dass der Host nur |
| | 183 | ueber HTTPS angesprochen werden darf. Zertifikatswarnungen sind dann nicht |
| | 184 | mehr per Klick uebergehbar. |
| | 185 | |
| | 186 | `includeSubDomains` erweitert die Policy auf die Subdomains '''dieses''' |
| | 187 | Hosts, nicht auf Geschwister-Hosts. Ein Header von `cloud.vpn21.net` gilt |
| | 188 | fuer `*.cloud.vpn21.net`, nicht fuer `shop.vpn21.net`. Damit alle |
| | 189 | `*.vpn21.net` erfasst werden, muss der Header vom Apex `vpn21.net` kommen. |
| | 190 | |
| | 191 | Unbedenklich, solange die internen Dienste unter einer getrennten Domain |
| | 192 | liegen. '''Vor einer Migration der internen Dienste unter `vpn21.net` neu |
| | 193 | bewerten''' -- `includeSubDomains` wuerde sie dann mit erfassen und HTTP |
| | 194 | dort abschalten. |
| | 195 | |
| | 196 | Das woechentliche HTTP-Fenster fuer Let's Encrypt ist nicht betroffen: HSTS |
| | 197 | ist eine Browser-Policy, der ACME-Validator wertet sie nicht aus, |
| | 198 | HTTP-01-Challenges funktionieren unveraendert. |
| | 199 | |
| | 200 | `preload` ist eine separate, nur schwer rueckgaengig zu machende |
| | 201 | Entscheidung und bleibt vorerst aussen vor. |
| | 202 | |
| | 203 | == Namensraum p21.net -- offener Punkt == |
| | 204 | |
| | 205 | Die Domain `p21.net` ist inzwischen oeffentlich vergeben und nicht unsere; |
| | 206 | die interne Nutzung ist knapp 30 Jahre alt und historisch gewachsen. |
| | 207 | Bisher 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, ausdruecklich 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 | |
| | 227 | Der ICANN-Draft zu `.internal` empfiehlt selbst die Subdomain einer eigenen |
| | 228 | Domain als erste Wahl und die reservierte TLD erst danach. |