| Version 1 (modified by , 12 days ago) (diff) |
|---|
nginx-header-diff.sh
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: SecurityHeader? (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 <site>-headers.inc. Das Skript findet die
Bloecke, in denen das fehlt, und zeigt, welche Zeilen danach ueberfluessig
sind.
Zwei Einsatzfaelle:
- Migration. Eine bestehende
.confauf 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
nginx-header-diff.sh [-p PREFIX] [-d DIREKTIVE] <datei.conf> <datei.inc>
| 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:
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 alwaysund"nosniff" alwaysgelten daher als identisch. alwayszaehlt zum Wert.add_header X-Content-Type-Options "nosniff";gegen"nosniff" alwaysim Include ergibt[KONFLIKT], nicht[DUPLIKAT]. Das ist Absicht: ohnealwayswird 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
- Skript laufen lassen, Ausgabe aufheben.
include conf.d/<site>-headers.inc;als erste Zeile in jeden gemeldeten Block eintragen, in dem es fehlt.nginx -t- Erst jetzt die
[DUPLIKAT]-Zeilen loeschen,[KONFLIKT]-Zeilen einzeln entscheiden. nginx -t, dannnginx -s reload- Skript erneut laufen lassen -- es sollten nur noch
[NUR-CONF]und bewusst behaltene[KONFLIKT]uebrig sein. - Pro Block-Typ mit
curl -sIgegenpruefen.nginx -tprueft 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:
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.confoder 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:
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:
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:
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/*:
#!/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.
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
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 fehlendeincludeeinsetzen, mit-nfuer einen Trockenlauf und einer.bak-Kopie. Bei den derzeit drei betroffenen.conf-Dateien ist Handarbeit sicherer. Fuer die Vorlagen-Nutzung spaeter neu bewerten. - Mehrere
.incgegen eine.conf. Derzeit genau eine Include-Datei pro Aufruf.
}}}
Attachments (1)
- nginx-header-diff.sh (10.6 KB) - added by 12 days ago.
Download all attachments as: .zip
