WordPress: SSL und Content Security Policy Header
Kürzlich haben wir dieser Seite ein SSL Zertifikat verpasst und einige Content Security Policy Header gesetzt, damit die Daten verschlüsselt und sicher übertragen werden können. Dieser Blogbeitrag beschreibt, wie wir bei der Umstellung des WordPress Blogs auf SSL vorgegangen sind.
Verwendung der hier gemachten Angaben auf eigenen Gefahr – keine Haftung! Mach vorher ein Backup!
Anforderungen
Erstmal kurz die Anforderungen, die wir uns selbst gestellt haben:
- Webseite nur noch per SSL ausliefern
- Security Header senden
- Gute Ergebnisse für die SSL Einbindung erzielen
- Besonderheiten von WordPress, Google Adsense und Affilinet beachten
Step 1: Das SSL Zertifikat erstellen
Aus Kostengründen und weil es ja gerade modern ist, haben wir uns für ein Zertifikat von Let’s Encrypt entschieden. Aus Faulheit und Desinteresse, das dort empfohle Python Script zu installieren, haben wir das Zertifikat online mit dem Tool von Zerossl. Damit lässt sich schnell und einfach ein Let’s Encrypt Zertifikat erstellen, auch wenn es nicht unbedingt sicher ist, man weiß nicht, ob die bei dem Prozess Schlüssel speichern (was nicht sooo gut wäre). Wir haben dabei ein RSA 4096 bits (SHA256withRSA) gewählt. Danach haben wir das Zertifikat auf dem Server installiert – das ist je nach Webserver und Hosting Anbieter unterschiedlich zu erledigen, daher lassen wir diesen Punkt mal aus und verweisen auf die Anleitungen des Hosters/der Webserver Software.
Step 2: WordPress auf SSL umstellen
Das ist recht einfach, in der WordPress Administration im Menüpunkt Einstellungen -> Allgemein -> WordPress-Adresse (URL) + Website-Adresse (URL) einfach das http: in https: umschreiben, das war es erstmal, die Seite wird nun über SSL ausgeliefert. Die WordPress internen Weiterleitungs Methoden erledigen im allgemein schon den nötigen Redirect von der normalen zur SSL Version. Es macht auch Sinn, die HTTP Antwort Header des Servers zu testen, zum einen die Version ohne SSL (ob diese korreckt auf die SSL Version weiterleitet), zum anderen die SSL Version, ob diese korreckte Antwort Header sendet. Zum Beispiel mit unserem Tool zum Header auslesen.
Mögliche „Fallen“ bei der SSL Umstellung
Nach dieser Umstellung sollte man sich die eigenen Seiten ansehen und dabei folgende Punkte beachten: Wird in der Browser Adressleiste ein Schloss angezeigt? Stimmen die Canonical Links? Und insbesondere, werden alle Inhalte angezeigt? Man sollte auf einer SSL Seite alle Elemente wie Bilder, externe Scripts und Stylesheets oder sonstige Medien per SSL eingebunden haben, sonst werden diese eventuell nicht angezeigt (Fehlermeldung „gemischte Inhalte“ im Browser). Wir hatten zum Beispiel die Schwierigkeit, die Affilinet Daten waren nicht per SSL eingebunden und mussten dies umstellen. Wie wird php im Server eingebunden? Bei Subsurvern wie php-fpm oder php-fastcgi werden je nach Einbindungsart die vom apache mod_header gesetzten Header wieder abgeschnitten – in diesem Fall ganz unten die php-fpm/fastcgi Lösung verwenden. Last but not least sollte man ggf. auch an Suchmaschinen denken, eine Umstellung von http auf https hat ähnliche Auswirkungen, wie ein Domainumzug!
Step 3: SSL Zertifikat testen
Um fest zu stellen, dass das Zertifikat eine sichere Verschlüsselung für möglichst viele Clients ermöglicht, bietet sich ein Test bei den SSL Labs von Qualis an. Das Ergebnis hierbei hängt von der Konfiguration der SSL Software (oft OpenSSL) und diversen Einstellungen bei der SSL Einbindung im Webserver ab, z.B. zugelassene SSL Protokolle (kein SSL1, SSL2, SSL3, daüfr TLS 1.2), den erlaubten Cipher Suites und deren Reihenfolge, Nutzung von Perfect Forward Secrecy (PFS) etc.
Man sollte hier ein gutes Ergebnis anpeilen, wir bekommen zum Testzeitpunkt ein A:

Beim Mozilla Observatory findet man stets aktuelle Konfigurations-Optionen für SSL Einbindungen. Hier zum Beispiel die momentan ratsamen Ciphersuites:
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
Step 4: Erweiterter SSL und Security Header Test
Leider ist nur das SSL Zertifikat zu testen, nicht ausreichend, denn wir wollten ja auch die Security Header einsetzen. Daher auf zum nächsten Test mit dem Tool von Mozilla Observatory. Unser Ergebnis – sehr schlecht:

Ganz klar, hier besteht eindeutig Verbesserungspotential!
Step 5: HTTP Strict Transport Security (HSTS) Header senden
Verbesserung bietet das Senden eines HSTS Headers, dieser teilt dem Browser mit, die Seite immer per SSL abzurufen. Zusätzlich kann man per „preload“ Anweisung diese Information in einer clientseitigen Browser Datenbank ablegen. Das bewirkt aber gewisse Risiken, denn der Client wird die Seite und ggf. sogar sämtliche Subdomains nicht mehr ohne SSL erreichen können. Daher haben wir darauf vorerst verzichtet und den Header ohne preload gesetzt. Dazu haben wir ganz einfach in der .htaccess Datei im WordPress Startverzeichnis folgendes eingetragen:
<IfModule mod_headers.c> Header set Strict-Transport-Security "max-age=31536000" env=HTTPS </IfModule>
HTTP Response Header vorher:
HTTP/1.1 200 OK Date: Wed, 25 Jan 2017 10:16:05 GMT Server: Apache Link: <https://edelstein-juwelen.de/wp-json/>; rel="https://api.w.org/" Link: <https://edelstein-juwelen.de/>; rel=shortlink Content-Encoding: gzip Vary: Accept-Encoding Cache-Control: max-age=86400 Expires: Thu, 26 Jan 2017 10:16:05 GMT Content-Length: 6354 Content-Type: text/html; charset=UTF-8
HTTP Response Header mit HSTS Header:
HTTP/1.1 200 OK Date: Wed, 25 Jan 2017 11:43:18 GMT Server: Apache Link: <https://edelstein-juwelen.de/wp-json/>; rel="https://api.w.org/" Link: <https://edelstein-juwelen.de/>; rel=shortlink Content-Encoding: gzip Vary: Accept-Encoding Cache-Control: max-age=86400 Expires: Thu, 26 Jan 2017 11:43:18 GMT Strict-Transport-Security: max-age=31536000 Content-Length: 6396 Content-Type: text/html; charset=UTF-8
Bereits diese kleine Änderung lässt den Wert beim Mozilla Observatory von F auf D steigen!
Step 6: XSS-Protection
Als nächstes setzen wir wieder einen Header (in den selben IfModule Block) gegen XSS Attacken und prüfen ihn mit dem Header Tool:
Header set X-XSS-Protection "1; mode=block"
Diese kleine Änderung lässt den Wert beim Mozilla Observatory auf C– steigen!
Step 7: X-Frame-Options gegen Frame Einbindung von fremden Seiten
Wir wollen nicht, das unsere Seiten in Frames oder Iframes fremder Seiten eingebetten werden können. Daher setzen wir nun den nächsten Header wieder in den selben IfModule Block:
Header set X-Frame-Options "sameorigin"
Dieser Header lässt den Wert beim Mozilla Observatory auf B– steigen!
Step 8: X-Content-Type-Options Header setzen
Als nächstes setzen wir einen Header, welcher den Browser anweist, nicht zu versuchen, den MIME Type zu erraten. Wieder in den gleichen IfModule Block:
Header set X-Content-Type-Options "nosniff"
Damit steigt der Mozilla Observatory Wert auf B (ohne Minus)
Step 9: X-Permitted-Cross-Domain-Policies Header setzen
Dieser Header verhindert das Einbetten externer Quellen in Adobe Produkten wie Flash oder PDF Reader, nur für den Fall….
Header set X-Permitted-Cross-Domain-Policies "none"
Dieser Header lässt den Mozilla Observatory Wert leider nicht weiter steigen.
Step 10: Referer nicht an fremde Seiten schicken
Ein weiterer Header für mehr Sicherheit verbessert das Senden des Referers (vorherige besuchte Seite) an andere Domains.
Header set Referrer-Policy "no-referrer-when-downgrade"
Auch dieser Header lässt den Mozilla Observatory Wert leider nicht weiter steigen.
Step 11: Die Content Security Policy einschalten
Jetzt wird es kompliziert, denn nicht nur, dass dieser Header mit bedacht zu wählen ist, es gibt obendrein noch zwei veraltete Varianten, die man wegen der Kompatibilität mit älteren Browsern jedoch auch setzen sollte. Wir setzen also diesmal drei Header, aber alle mit den (fast) gleichen (sehr restriktiven) Werten für die Content-Security-Policy.
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self'; child-src 'self'; font-src 'self'; connect-src 'self'" Header set X-Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self'; child-src 'self'; font-src 'self'; connect-src 'self'" Header set X-WebKit-CSP "default-src 'self'; script-src 'self'; object-src 'self'; style-src 'self'; img-src 'self' data:; media-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'"
Die gute Nachricht:
Dies bringt uns beim Mozilla Observatory auf ein schön grünes A+!


Die schlechte Nachricht:
Das WordPress bricht (je nach Theme mehr oder weniger) zusammen – nichts geht mehr! Was ist da denn passiert? Ganz klar, die Content-Security-Policy Werte sind extrem strikt gesetzt, WordPress kann nun leider weder CSS noch Javascript inline ausführen oder von externen Quellen laden. Im WordPress Administrationsbereich ist es besonders schlimm, dort geht kaum noch eine Einstellung. Man muss also einen Kompromiss zwischen Sicherheit und Funktion finden.
Step 12: Der Kompromiss
Die Seite muss ja weiterhin funktionieren. Daher lockern wir die CSP Werte respektive. Die Details in den Einstellungen können je nach verwendetem Theme abweichen, je nach Anforderungen „strenger“ oder „lockerer“ zu einem besserem Ergebnis führen. In unserem Fall mussten Resourcen von Adsense und Affilinet berücksichtigt werden. Mehr zu Content Security Policy Einstellungen gibt es auf der CSP Webseite.
Mit unseren verwendete CSP Header kommen wir zu einem B+, wenn alles auf den (Front-)Seiten funktioniert:

Hier die von uns dazu verwendeten CSP-Header:
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-WebKit-CSP "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; frame-src 'self' https:; font-src 'self' data:; connect-src 'self'"
Die Header sind erheblich „weicher“ als die vorherige Version, besser wäre es, explizit nur die verwendeten Domains frei zu geben, statt alles mit https Protokoll. Aber dies ist mit den wehcselnden Werbemitteln und damit wechselnden Domains der Werbeeinblendungen in unserem Fall nicht praktikabel.
Step 13: Worpress Administration und die eval() Funktion
Die ganze Frontseite funktioniert jetzt mit recht guten Werten. Aber natürlich macht die WordPress Administration noch ein wenig Ärger. Dort werden als besonders unsicher bezeichnete Javascript Funktionen wie eval() verwendet. Damit die WordPress Administration funktioniert, müssen daher die CSP Header erweitert werden, um diese Funktion auch zuzulassen. Dazu haben wir die soeben erstellten Header erweitert und diese in der neu angelegten wp-admin/.htaccess eingetragen:
<IfModule mod_headers.c> Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-WebKit-CSP "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; frame-src 'self' https:; font-src 'self' data:; connect-src 'self'" </IfModule>
Damit funktioniert der WordPress Administrationsbereich bei uns auch wieder reibungslos.
Zusammenfassung
WordPress lässt sich per SSL und einigen Headern trotz kleinen Tücken doch recht gut verbessern. Hier noch einmal der ganze Header-Block für die .htaccess Datei:
<IfModule mod_headers.c> Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'sel f' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 's elf' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-WebKit-CSP "default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: ht tps:; media-src 'self'; frame-src 'self' https:; font-src 'self' data:; connect-src 'self'" Header set X-Content-Type-Options "nosniff" Header set X-Frame-Options "sameorigin" Header set X-XSS-Protection "1; mode=block" Header set Strict-Transport-Security "max-age=31536000" env=HTTPS Header set X-Permitted-Cross-Domain-Policies "none" Header set Referrer-Policy "no-referrer-when-downgrade" </IfModule>
Besonderheit: php-fpm/fastcgi entfernt Apache Header
Es nennt sich nicht Bug, sondern Feature – die Eigenschaft von Subservern (fastcgi, php-fpm), die Apache Header zu entfernen. Daher müssen in diesem Fall die Header zusätzlich (man will ja bei allen Dateien diese Header, nicht nur bei php) per php gesetzt werden. Dies geschieht am besten mit ein bisschen Code in der function.php des Themes (oder man nutzt WordPress Plugins dafür):
function send_security_header ( ) { header ( "Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" ); header ( "X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; child-src 'self' https:; font-src 'self' data:; connect-src 'self'" ); header ( "X-WebKit-CSP: default-src 'self'; script-src 'self' 'unsafe-inline' https:; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self'; frame-src 'self' https:; font-src 'self' data:; connect-src 'self'" ); header ( "X-Content-Type-Options: nosniff" ); header ( "X-Frame-Options: sameorigin" ); header ( "X-XSS-Protection: 1; mode=block" ); header ( "Strict-Transport-Security: max-age=31536000" ); header ( "X-Permitted-Cross-Domain-Policies: none" ); header ( "Referrer-Policy: no-referrer-when-downgrade" ); } add_action ( 'send_headers', 'send_security_header' );
Viel Erfolg bei der Umstelung / Verbesserung der SSL Implementation Ihrer Webseite! Fragen oder Anregungen – Kommentar schreiben!
Geschrieben in: Aktuelles | 3 Kommentare »
3 Kommentare zu “WordPress: SSL und Content Security Policy Header”
Trackbacks/Pingbacks