= nginx-header-diff.sh = Das Script liegt als Anhang bei, sollte aber normal auch im `/etc/nginx/conf.d`-Ordner des Servers liegen. [[PageOutline]] Hilfsskript fuer die Migration und Pflege der Security-Header in der NGINX-Konfiguration. Vergleicht die `add_header`-Direktiven einer `.conf`-Datei mit dem effektiven Header-Satz einer Include-Datei und meldet je Block, was doppelt, was abweichend und was nur in der `.conf` steht. Siehe auch: WebDiensteNginxSecurityHeader (Begruendung der einzelnen Header und die Vererbungsregel von `add_header`). == Wozu == `add_header` wird von der uebergeordneten Ebene nur dann geerbt, wenn auf der aktuellen Ebene '''kein einziges''' `add_header` steht. Jeder `location`-Block mit eigenem `add_header` braucht deshalb ein eigenes `include` der passenden `-headers.inc`. Das Skript findet die Bloecke, in denen das fehlt, und zeigt, welche Zeilen danach ueberfluessig sind. Zwei Einsatzfaelle: * '''Migration.''' Eine bestehende `.conf` auf die Include-Struktur umstellen. * '''Vorlagen.''' Eine fremde Beispielkonfiguration (etwa die offizielle von Nextcloud) uebernehmen und die mitgelieferten Header gegen die eigenen abgleichen. Das Skript aendert '''nichts'''. Es zeigt nur an. == Aufruf == {{{#!sh nginx-header-diff.sh [-p PREFIX] [-d DIREKTIVE] }}} ||= Option =||= Bedeutung =|| || `-p PREFIX` || Basisverzeichnis fuer relative `include`-Pfade. Vorgabe `/etc/nginx`. || || `-d DIREKTIVE` || Zusaetzliche Direktive beruecksichtigen, mehrfach angebbar. Vorgabe nur `add_header`. Sinnvoll: `-d proxy_hide_header`. || || `-h` || Kurzhilfe. || Beispiele: {{{#!sh cd /etc/nginx ./nginx-header-diff.sh cloudservice.conf conf.d/nextcloud-headers.inc ./nginx-header-diff.sh -d proxy_hide_header usvn.conf conf.d/usvn-headers.inc ./nginx-header-diff.sh -p /etc/nginx elager.p21.net.conf conf.d/intern-headers.inc }}} Rueckgabewert 2 bei Aufruffehlern oder nicht lesbaren Dateien, sonst 0. Der Rueckgabewert sagt '''nichts''' darueber aus, ob Abweichungen gefunden wurden. == Ausgabe == Zwei Abschnitte. Zuerst der effektive Header-Satz der Include-Datei, nachdem alle `include`-Ketten rekursiv aufgeloest wurden -- in eckigen Klammern die Datei und Zeile, aus der der Header tatsaechlich stammt: {{{ === Effektiver Header-Satz aus conf.d/nextcloud-headers.inc === add_header X-Content-Type-Options "nosniff" always; [/etc/nginx/conf.d/base-headers.inc:54] add_header X-Frame-Options "SAMEORIGIN" always; [/etc/nginx/conf.d/base-headers.inc:56] add_header X-Robots-Tag "noindex, nofollow" always; [conf.d/nextcloud-headers.inc:35] }}} Danach die `.conf`, nach Bloecken gegliedert. Bloecke ohne `add_header` tauchen nicht auf -- die brauchen auch kein `include`: {{{ === cloudservice.conf === server (Zeile 2) include nextcloud-headers.inc: FEHLT -> einfuegen cloudservice.conf:7: add_header X-Content-Type-Options "nosniff" always; [DUPLIKAT] cloudservice.conf:8: add_header Referrer-Policy "no-referrer" always; [NUR-CONF] location ~ \.(?:css|js|svg|gif)$ (Zeile 24) include nextcloud-headers.inc: FEHLT -> einfuegen cloudservice.conf:27: add_header X-Robots-Tag "none" always; [KONFLIKT] Include hat: add_header X-Robots-Tag "noindex, nofollow" always; --- DUPLIKAT 5 KONFLIKT 1 NUR-CONF 5 }}} Die Fundstellen folgen dem Format von `grep -n`: `datei:zeile: text`. Zitiert wird in der Originalschreibweise der jeweiligen Datei, einschliesslich Semikolon -- die Zeilen lassen sich also direkt kopieren. === Markierungen === ||= Marke =||= Bedeutung =||= Massnahme =|| || `[DUPLIKAT]` || Name und Wert identisch mit dem Include || Zeile loeschen, sobald das `include` steht || || `[KONFLIKT]` || Name gleich, Wert abweichend || von Hand entscheiden || || `[NUR-CONF]` || im Include nicht enthalten || Zeile behalten || === Vergleichsregeln === * Header-'''Namen''' werden case-insensitiv verglichen, wie es HTTP vorsieht. Angezeigt wird die Schreibweise aus der Datei. * '''Werte''' werden vor dem Vergleich normiert: Anfuehrungszeichen entfernt, Whitespace zusammengefasst. `nosniff always` und `"nosniff" always` gelten daher als identisch. * `always` zaehlt zum Wert. `add_header X-Content-Type-Options "nosniff";` gegen `"nosniff" always` im Include ergibt `[KONFLIKT]`, nicht `[DUPLIKAT]`. Das ist Absicht: ohne `always` wird der Header bei Fehlerseiten und Redirects nicht gesendet, der Unterschied ist real. Beim Aufraeumen ist es trotzdem fast immer ein Loeschkandidat -- aber einer, der gesehen werden soll. == Arbeitsablauf bei der Migration == 1. Skript laufen lassen, Ausgabe aufheben. 2. `include conf.d/-headers.inc;` als '''erste''' Zeile in jeden gemeldeten Block eintragen, in dem es fehlt. 3. `nginx -t` 4. Erst jetzt die `[DUPLIKAT]`-Zeilen loeschen, `[KONFLIKT]`-Zeilen einzeln entscheiden. 5. `nginx -t`, dann `nginx -s reload` 6. Skript erneut laufen lassen -- es sollten nur noch `[NUR-CONF]` und bewusst behaltene `[KONFLIKT]` uebrig sein. 7. Pro Block-Typ mit `curl -sI` gegenpruefen. `nginx -t` prueft Syntax, nicht Wirkung. '''Reihenfolge beachten:''' erst das `include` einfuegen, dann die Duplikate loeschen. Umgekehrt steht die Konfiguration zwischenzeitlich ohne Security-Header da. Pruefung mit curl, jeweils gegen genau den Pfad, den der Block bedient: {{{#!sh 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 }}} Im Browser auf den Cache achten -- angezeigt werden die Header der urspruenglichen Antwort. Mit Shift-Reload gegenpruefen. == Grenzen == * `include`-Pfade mit Platzhaltern (`conf.d/*.inc`) werden nicht aufgeloest. * Geschweifte Klammern innerhalb von Direktivwerten -- etwa in einem `log_format` -- koennen die Blockerkennung stoeren. * Include-Zyklen werden erkannt und mit einer Warnung auf stderr uebersprungen; Verschachtelungstiefe maximal 16. * Nicht lesbare Include-Dateien erzeugen eine Warnung auf stderr, das Skript laeuft weiter -- der effektive Header-Satz ist dann unvollstaendig. Warnungen also nicht uebersehen. * Das Skript kennt nur die Datei, die ihm uebergeben wird. Header aus `nginx.conf` oder aus anderen per Glob eingebundenen Dateien sieht es nicht. * Der Abgleich der `include`-Zeilen erfolgt ueber den '''Basisnamen''' der Include-Datei. Zwei gleichnamige Includes in verschiedenen Verzeichnissen werden nicht unterschieden. == Gegenprobe ueber die Gesamtkonfiguration == Das Skript kennt nur die Datei, die ihm uebergeben wird. Header aus `nginx.conf` selbst oder aus anderen per Glob eingebundenen Dateien sieht es nicht -- und genau solche erben in den `server`-Block hinein. Wenn nach der Migration ein Header fehlt oder doppelt auftaucht, den das Skript nicht gemeldet hat, liegt die Ursache ausserhalb seines Blickfelds. Die folgenden Pruefungen schliessen diese Luecke. === Alle add_header in der entfalteten Konfiguration === `nginx -T` gibt die Konfiguration inklusive aller aufgeloesten `include` aus: {{{#!sh nginx -T | grep -n 'add_header\|proxy_hide_header' }}} '''Vorsicht:''' die Ausgabe enthaelt die kompletten Konfigurationsdateien, unter Umstaenden also auch Zugangsdaten aus `proxy_pass`-Zielen, `auth_basic_user_file`-Pfaden oder aehnlichem. Nicht unbesehen in Tickets oder Foren kopieren. === Bloecke mit add_header finden === Listet jeden `location`- oder `server`-Block, in dem mindestens ein `add_header` steht -- also jeden Block, der ein `include` braucht: {{{#!sh nginx -T | awk '/^[[:space:]]*(location|server)/ {loc=$0; has=0} /add_header/ {if (loc && !has) {print loc; has=1}}' }}} Bei einer frisch migrierten Konfiguration sollte zu jedem hier gelisteten Block auch ein `include` gehoeren. Das Skript prueft das pro Datei, dieser Aufruf prueft es ueber alles. === Doppelte Header in der Antwort === Faengt die Faelle, in denen die Anwendung ihre Header selbst setzt und NGINX sie zusaetzlich anhaengt -- Nextcloud aus PHP, USVN aus dem Apache. Gibt die Namen aus, die mehr als einmal vorkommen: {{{#!sh curl -sI https://cloud.vpn21.net/nextcloud/index.php \ | awk -F: '/^[A-Za-z-]+:/ {print tolower($1)}' | sort | uniq -d }}} Leere Ausgabe ist das Ziel. `set-cookie` darf legitim mehrfach auftreten und ist kein Befund. Gegenmittel: `fastcgi_param modHeadersAvailable true;` bei Nextcloud, `proxy_hide_header` bei Apache-Backends -- siehe SecurityHeader. === Header-Satz pro URL pruefen === Prueft mehrere Endpunkte gegen die erwartete Liste. Jeder Block-Typ einer Site braucht einen eigenen Eintrag: PHP, Assets, Fonts, statische Dateien, `/.well-known/*`: {{{#!sh #!/bin/sh erwartet='x-content-type-options x-frame-options x-permitted-cross-domain-policies referrer-policy' for u in \ https://www.vpn21.net/ \ https://cloud.vpn21.net/ \ https://cloud.vpn21.net/nextcloud/index.php \ https://cloud.vpn21.net/nextcloud/core/css/server.css do echo "== $u" h=$(curl -sI "$u" | tr 'A-Z' 'a-z') echo "$h" | head -1 | sed 's/^/ /' for k in $erwartet; do echo "$h" | grep -q "^$k:" || echo " FEHLT: $k" done done }}} Bewusst ohne `curl -L`: ein Redirect soll '''nicht''' verfolgt werden, sondern selbst geprueft werden. === Redirects und Fehlerseiten === Der haeufigste blinde Fleck. Ohne `always` sendet NGINX einen Header nur bei den Statuscodes 200, 201, 204, 206, 301, 302, 303, 304, 307 und 308 -- bei 403, 404 oder 500 also nicht. Deshalb steht in den Include-Dateien ueberall `always`. {{{#!sh curl -sI https://cloud.vpn21.net/gibtsnicht # erwartet 404 curl -sI https://irgendwas.vpn21.net/ # Catch-all-Redirect }}} In beiden Antworten muss der vollstaendige Header-Satz stehen. Fehlt er beim 404, aber nicht beim 200, fehlt irgendwo ein `always`. === Nach jeder Aenderung === {{{#!sh nginx -t && nginx -s reload }}} `nginx -t` prueft die Syntax, nicht die Wirkung. Die Wirkung zeigt nur `curl`. == Technisches == Bash-Wrapper um mehrere AWK-Programme. Keine Abhaengigkeiten ausser einer POSIX-Shell und `awk`; getestet mit `mawk` 1.3.4. Der Scanner arbeitet zeichenweise mit Quote-Status. Das ist noetig, weil `;` `{` `}` innerhalb von Anfuehrungszeichen vorkommen und eine naive Trennung die Direktive zerreisst -- etwa bei `"max-age=63072000; includeSubDomains"` oder `"1; mode=block"`. Direktiven ueber mehrere Zeilen werden zusammengefasst und mit der Zeilennummer ihres Anfangs gemeldet. Kommentare werden ignoriert, auch am Zeilenende, aber nicht innerhalb von Anfuehrungszeichen. == Offen == * '''Patch-Modus.''' Technisch machbar: `[DUPLIKAT]`-Zeilen entfernen und fehlende `include` einsetzen, mit `-n` fuer einen Trockenlauf und einer `.bak`-Kopie. Bei den derzeit drei betroffenen `.conf`-Dateien ist Handarbeit sicherer. Fuer die Vorlagen-Nutzung spaeter neu bewerten. * '''Mehrere `.inc` gegen eine `.conf`.''' Derzeit genau eine Include-Datei pro Aufruf. }}}