Content Security Policy in Nginx
Eine CSP in Nginx zu deployen ist üblicherweise ein Einzeiler in deinem Server-Block. Diese Anleitung deckt die richtige Platzierung ab, das always-Flag das in vielen Tutorials unter den Tisch fällt, und die Trückel rund um Header-Vererbung in verschachtelten Locations.
Das minimale Snippet
Öffne die Konfigurationsdatei deiner Site (auf Debian/Ubuntu meist /etc/nginx/sites-enabled/deine-seite, auf RHEL/CentOS oft /etc/nginx/conf.d/deine-seite.conf) und füge Folgendes in den server { }-Block ein:
server {
listen 443 ssl http2;
server_name example.com;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'" always;
# ... Rest der Config ...
}
Danach validieren und neu laden:
sudo nginx -t
sudo systemctl reload nginx
Reload (nicht Restart) reicht für Header-Änderungen völlig aus und schmeißt keine bestehenden Verbindungen weg.
Warum das always-Flag entscheidend ist
Das ist das am häufigsten vergessene Detail in Nginx-CSP-Tutorials — und ein echtes Sicherheitsproblem.
Ohne always wendet Nginx add_header-Direktiven nur auf 200-, 201-, 204-, 206-, 301-, 302-, 303-, 304-, 307- und 308-Responses an. Heisst deine 404-Not-Found-, 403-Forbidden- und 500-Internal-Server-Error-Seiten haben gar keinen CSP-Header. Solche Fehlerseiten spiegeln oft benutzerkontrollierten Inhalt zurück (denk an eine falsch eingegebene URL die in die Seite reingedruckt wird) — genau dort lauern XSS-Bugs.
Das always-Flag zwingt Nginx, den Header auf jeden Response anzuwenden, egal welcher Status-Code. Bei Security-Headern ist es Pflicht.
Report-Only-Rollout
Deploye den Header zuerst als Content-Security-Policy-Report-Only. Der Browser protokolliert Verstöße in der Konsole, blockiert aber nichts. So kannst du 1–2 Wochen beobachten und die Warnungen aufräumen, bevor du zum Durchsetzen wechselst. Seite öffnen, F12 drücken, und nach Zeilen wie dieser suchen:
[Report Only] Refused to load the script 'https://cdn.example.com/widget.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'".
Jede Zeile ist eine echte Ressource die deine Seite lädt und die die Policy blockieren würde. Entweder den Origin freigeben oder die Abhängigkeit loswerden. Sobald die Konsole ruhig ist, den Header-Namen von Content-Security-Policy-Report-Only zu Content-Security-Policy ändern und Nginx reloaden.
Typische Fehlerquellen
Verschachtelte add_header-Direktiven löschen sich gegenseitig
Diese Falle erwischt jeden mindestens einmal. Wenn du add_header sowohl im server-Block UND in einem verschachtelten location-Block hast, ersetzen die Header im location-Block die aus dem Parent-Context komplett — sie werden nicht gemerged. Ein einzelner Header in einem location löscht also still und heimlich alle Header aus dem Parent-server.
server {
add_header X-Frame-Options DENY always;
add_header Content-Security-Policy "default-src 'self'" always;
location /api/ {
add_header X-API-Version 1 always;
# FALSCH: X-Frame-Options und CSP werden hier NICHT angewendet.
# Du musst sie hier wiederholen.
}
}
Lösung: Alle Security-Header in jedem location-Block wiederholen der auch nur einen einzigen Header hinzufügt. Oder alternativ eine map-Variable + eine einzige add_header-Direktive im Server-Level, um die Duplizierung zu vermeiden.
Der Header taucht gar nicht auf
Mit curl -I https://deine-seite.example verifizieren. Wenn die CSP im Output fehlt, prüfe:
- Läuft
nginx -tsauber durch? Syntaxfehler werden stillschweigend übersprungen. - Trifft dein Request den richtigen Server-Block? Prüfe das
server_name-Matching. - Fängt ein
location-Block den Request zuerst ab und überschreibt damit die Header? (Siehe oben.) - Läuft ein Proxy oder CDN vor Nginx das die Header abschneidet? Cloudflare zum Beispiel entfernt CSP im Normalfall nicht, aber falsch konfigurierte Reverse-Proxies tun das manchmal.
Mehrere CSP-Header gleichzeitig
Wenn du mehrere add_header Content-Security-Policy ...-Direktiven hast (typischerweise aus Include-Dateien), sendet Nginx alle. Browser behandeln mehrere CSP-Header als Schnittmenge, was restriktiver ist als jede einzelne Policy. Resultat: Sachen werden blockiert die du nicht erwartet hast. Zu einem einzigen Header konsolidieren.
HTTP/2 und Groß-/Kleinschreibung
HTTP/2 verlangt technisch gesehen Header-Namen in Kleinbuchstaben. Nginx sendet content-security-policy sowieso bereits kleingeschrieben auf dem Draht — egal wie du es in der Config schreibst — also ist das selten ein Problem. Aber falls du mit Tools debuggst die Header wörtlich darstellen, lass dich nicht vom Case-Unterschied verwirren.
Fortgeschritten: map für bedingte Policies
Wenn du unterschiedliche CSPs für verschiedene Teile deiner Seite willst — etwa strikt auf den Marketing-Seiten, lockerer im Admin-Panel — nutze eine map-Direktive im http { }-Kontext:
map $request_uri $csp_policy {
default "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
~^/admin/ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'";
}
server {
add_header Content-Security-Policy $csp_policy always;
}
So bleibt ein einziger add_header-Aufruf bestehen und die passende Policy wird auf den jeweiligen Pfad geroutet.