wiki:WebDiensteNginxSecurityHeaderScript

Version 2 (modified by Johannes Lode, 12 days ago) (diff)

--

nginx-header-diff.sh

Das Script liegt als Anhang bei, sollte aber normal auch im /etc/nginx/conf.d-Ordner des Servers liegen.

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 <site>-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

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 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/<site>-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:

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:

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 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.

}}}

Attachments (1)

Download all attachments as: .zip