Content Security Policy in Apache (.htaccess)

Eine Content Security Policy schränkt ein, welche Ressourcen der Browser auf Ihrer Apache-Website laden darf. Diese Anleitung zeigt das minimale, funktionierende Deployment: mod_headers aktivieren, Header in .htaccess eintragen, im Report-Only-Modus testen, dann auf Enforcing umschalten. Fünf Minuten Ende-zu-Ende, sobald mod_headers läuft.

Voraussetzung: mod_headers aktivieren

Apaches mod_headers-Modul ist das was der .htaccess erlaubt, Response-Header zu setzen. Auf Debian/Ubuntu-Derivaten:

sudo a2enmod headers
sudo systemctl restart apache2

Auf RHEL/CentOS/Fedora die /etc/httpd/conf/httpd.conf öffnen und sicherstellen, dass diese Zeile vorhanden und nicht auskommentiert ist:

LoadModule headers_module modules/mod_headers.so

Apache danach neu starten. Mit apachectl -M | grep headers prüfen Sie, ob das Modul wirklich geladen ist.

Das minimale .htaccess-Snippet

Öffnen Sie die .htaccess-Datei im Document-Root (oder legen Sie sie an, falls sie nicht existiert) und fügen Sie Folgendes ein:

<IfModule mod_headers.c>
  Header always set 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'"
</IfModule>

Ein paar Dinge zum Merken:

Report-Only: testen ohne etwas kaputtzumachen

Der Unterschied zwischen den beiden Header-Namen ist der einzige, auf den es beim Rollout ankommt:

# meldet nur — der Browser lädt alles, protokolliert aber jeden Verstoß
Header always set Content-Security-Policy-Report-Only "..."

# setzt durch — der Browser blockiert, was die Policy nicht erlaubt
Header always set Content-Security-Policy "..."

Die Policy-Syntax ist in beiden Fällen identisch. Sie können also eine Policy wochenlang im Report-Only-Modus beobachten und den Header-Namen erst ändern, wenn die Meldungen ausbleiben. Kein Besucher merkt in dieser Zeit etwas davon.

Beide Header gleichzeitig senden

Weniger bekannt, aber der eigentliche Grund, warum Report-Only mehr ist als eine Testphase: Apache darf beide Header setzen, und der Browser wertet sie unabhängig voneinander aus.

<IfModule mod_headers.c>
  # läuft seit Monaten, bleibt scharf
  Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'"

  # der Kandidat: ohne 'unsafe-inline', vorerst nur beobachtend
  Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'"
</IfModule>

Das ist der übliche Weg, eine bestehende Policy zu verschärfen: Die scharfe Policy schützt weiter, während die strengere daneben zeigt, was beim Umstellen brechen würde. Bleibt die Konsole bei der Report-Only-Variante über mehrere Wochen ruhig, tauschen Sie sie gegen die alte aus und löschen die Report-Only-Zeile.

Die Konsolenausgabe lesen

Seite neu laden, DevTools (F12) → Konsole. Sie sehen rote Warnungen für jede Ressource die blockiert werden würde, z. B.:

[Report Only] Refused to load the script 'https://cdn.example.com/analytics.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'".

Jede dieser Zeilen ist eine echte Ressource die Ihre Seite lädt und die die aktuelle Policy nicht abdeckt. Sie haben zwei Optionen: Domain in die passende Direktive aufnehmen, oder die Abhängigkeit loswerden. Iterieren bis die Konsole sauber ist.

Der schnellere Weg: Lassen Sie unseren kostenlosen Scanner über Ihre Seite laufen. Er crawlt jede Unterseite, findet alle externen Ressourcen und generiert die exakte CSP die Sie brauchen — ohne Trial and Error. Das Ergebnis können Sie direkt in die .htaccess einfügen.

In den Enforcing-Modus wechseln

Sobald die Konsole ruhig bleibt, ändern Sie den Header-Namen von Content-Security-Policy-Report-Only zu Content-Security-Policy und laden neu:

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
</IfModule>

Fertig. Ihre Seite setzt jetzt eine CSP durch. Ab diesem Moment wird jeder Verstoß vom Browser wirklich blockiert, nicht nur gemeldet.

Typische Fehlerquellen

„mod_headers is not loaded"

Wenn das im Apache-Error-Log auftaucht, haben Sie die Voraussetzung übersprungen. sudo a2enmod headers && sudo systemctl restart apache2 (Debian/Ubuntu) oder die LoadModule-Zeile in der httpd.conf auskommentieren.

Der Header ist da, aber die Policy wird ignoriert

Prüfen Sie, ob an einer anderen Stelle ein zweiter CSP-Header gesetzt wird (von PHP, einer VirtualHost-Datei, einer anderen .htaccess). Wenn mehrere Content-Security-Policy-Header gesendet werden, nehmen Browser die Schnittmenge — und die ist oft restriktiver als beabsichtigt. Mit curl -I https://ihre-seite.example sehen Sie alle Header. Duplikate eliminieren und nur den einen aus .htaccess behalten.

WordPress/PHP-Anwendungen brauchen 'unsafe-inline'

Die meisten CMS und PHP-Frameworks erzeugen Inline-<script>- und <style>-Blöcke. Ohne 'unsafe-inline' in script-src und style-src bricht die Seite. Die striktere Alternative wären Nonces oder Hashes, aber das erfordert Code-Anpassungen — Details in der WordPress-Anleitung.

CORS und CSP sind zwei verschiedene Dinge

Ein häufiges Missverständnis: Wenn Ihre Seite beim Laden einer Ressource blockiert wird, kann das CORS (Access-Control-Allow-Origin) sein und nicht CSP. CSP regelt was Ihre Seite laden darf; CORS regelt, ob der andere Server bereit ist, von Ihrer Seite aus geladen zu werden. Beide können Ressourcen blockieren, die Lösung ist jeweils eine andere.

Dev-Server mit Hot-Reload

Webpack Dev Server, Vite, Next.js im Dev-Modus — die injizieren alle Inline-Skripte für Hot Reloading. Wenn Ihre CSP das blockiert, bricht die Dev-Umgebung zusammen. Lösung: die strikte CSP nur in Produktion ausrollen. In Apache lässt sich das über SetEnvIf lösen, oder einfach indem die .htaccess mit der CSP nur in Produktion deployt wird.

VirtualHost statt .htaccess

Wer Root-Zugriff auf den Server hat, arbeitet nicht in der .htaccess, sondern im VirtualHost. Die Syntax ist dieselbe, nur der Ort ist ein anderer:

<VirtualHost *:443>
    ServerName ihre-seite.example
    DocumentRoot /var/www/ihre-seite/public

    <IfModule mod_headers.c>
      Header always set 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'"
    </IfModule>
</VirtualHost>

Wo diese Datei liegt, hängt von der Distribution ab:

Der eine Unterschied, der im Alltag zählt: Eine Änderung an der .htaccess wirkt beim nächsten Request, eine Änderung am VirtualHost erst nach einem Reload. Das macht die .htaccess zum besseren Ort während der Testphase und den VirtualHost zum besseren, sobald die Policy steht — dort kann kein versehentlich hochgeladenes Verzeichnis sie überschreiben.

Prüfen Sie vor jedem Reload mit sudo apachectl configtest, ob die Datei fehlerfrei ist. Ein Syntaxfehler im VirtualHost verhindert den Start des gesamten Servers — anders als in der .htaccess, wo er nur einen 500er für dieses eine Verzeichnis auslöst.

Apache-spezifische Feinheiten

.htaccess und Performance

Jede Direktive in der .htaccess wird bei jedem Request neu ausgewertet — Apache sucht dabei in jedem Verzeichnis entlang des Pfades nach einer solchen Datei. Für ein einzelnes Header set ist dieser Aufwand nicht messbar; wer die .htaccess deswegen meidet, optimiert an der falschen Stelle. Spürbar wird es erst bei umfangreichen Rewrite-Regeln auf Seiten mit hoher Last. Dann lohnt sich der Umzug in den VirtualHost und AllowOverride None, was die Suche ganz abschaltet.

Header append vs. Header set

Nehmen Sie immer set, niemals append. append würde mehrere CSP-Werte aneinanderhängen, und Browser behandeln das als Schnittmenge — in der Regel nicht das was Sie wollen.

Reporting-Endpoints

Sobald Sie im Enforcing-Modus sind, lohnt sich eine report-uri-Direktive, damit Sie Verstöße sehen die draußen in freier Wildbahn passieren (nicht nur bei Ihren eigenen Tests):

Header always set Content-Security-Policy "default-src 'self'; ... ; report-uri https://ihr-reporting-endpunkt.example/csp"

Dienste wie report-uri.com bieten kostenlose Stufen; Sie können aber auch selber einen Collector hosten.

Ihre exakte CSP in 30 Sekunden generieren → Wir crawlen Ihre Seite, finden jede Ressource, und spucken einen .htaccess-Block aus der zu Ihrem echten Stack passt — kein Rumraten