Mehrere virtuelle Server mit nginx und PHP-FPM für WordPress (Teil 2 / 3)

Teil 1Teil 2Teil 3

Im zweiten Teil geht es um die individuelle Einrichtung der virtuellen Server für nginx.

Server oder virtual hosts?

Im Gegensatz zu den “virtual hosts” von Apache spricht man bei nginx von “servern“. Ich möchte das Aufgreifen und nutze im Folgenden einfach nur von “Server” wenn ich von einem individuellen Host oder virtuellem Server spreche. Wie bei Apache werden diese idealerweise in eigenständigen Konfig-Dateien definiert. Hier gibt es verschiedene Vorlieben, ob die Konfig-Dateien unter /etc/nginx/sites-available oder /etc/nginx/conf.d abgelegt werden.

Aus technischer Sicht macht es wirklich überhaupt gar keinen Unterschied. Bei der ersten Variante wird im Ordner /etc/nginx/sites-enabled mit einem symbolischen Link auf die tatsächliche Konfig-Datei an einem anderen Ort verwiesen. Um sie zu de-aktivieren, wird dann einfach der symbolische Link gelöscht. Das ist auch der klassische Apache-Weg.

Bei der zweiten Variante muss man die Konfig-Dateien im Order /etc/nginx/conf.d mit der Endung conf anlegen. Um den Server zu deaktivieren, entfernt man die Endung .conf. Entscheide selber, was dir lieber ist.

Eine beispielhafte Konfiguration für einen Server ist folgendermaßen aufgebaut. Die interessante Parameter beschreibe ich weiter unten etwas ausführlicher. Ich versuche möglichst viel mit Platzhaltern zu arbeiten (set $server “example_com;) um die Nutzung für neue Server zu vereinfachen. Leider funktioniert das bei nginx nicht für jeder Direktive. (So werden in nginx die Parameter genannt. Warum? Weil eine Direktive selber auch Parameter besitzen kann, wie du gleich sehen wirst.)

Außerdem habe aus Gründen der Übersicht sich wiederholdene Einstellungen in Dateien (sogenannte Snippets) ausgelagert. Diese befinden sich im Ordner /etc/nginx/snippets/. Diese Snippets werden an der entsprechenden Stelle mit include eingebunden.

Der Cache

Was soll gecached werden?

Im 1. Teil habe ich das Thema ja schon kurz angerissen und zwei Direktiven beschrieben. Auf Server-Ebene will ich den Cache nun noch etwa feiner einstellen. Zunächst geht es an ein paare globale Einstellungen, die ich im Snippet /etc/nginx/snippets/caching.conf abgelegt habe.

Nicht jede Anfrage darf gecached werden, wie z.B. POST-Requests, die ja tendentiel eher unterschiedliche Daten bei jeder Anfrage enthalten. Für diese Unterscheidung nutze ich die Variable $no_cache. So kann ich mit einfachen if-Abfragen festlegen, welche Requests vom Cache ignoriert werden sollen, wie z.B:

  • POST-Requests
  • Requests, die einen Query-String enthalten (GET)
  • Requests, deren URL auf ein bestimmtes Muster passen
  • Requests von eingeloggten Bentzern (WordPress-Spezifisch!)
  • Requests, bei denen das Cookie PHPSESSID gesetzt ist

Wie soll gecached werden?

Um den Zweck der Parameter hinter fastcgi_cache_path zu verstehen, werde ich grob erklären, wie der nginx-Cache funktioniert:

Über FastCGI wird zunächst die PHP-Datei an den PHP-Interpreter übergeben. Das Ergebnis, z.B. ein HTML-Dokument geht dann an den Empfänger. Ist diese Ressource als “cachable” markiert, legt nginx das zu Ergbnis außerdem in temporär in einen Ordner ab und kopiert es von dort in den eigentlich Cache-Ordner. Damit diese Resource später wiedergefunden wird, wird ein Schlüssel erstellt. Ein Liste (“Cache-Verzeichnis”) dieser Schlüssel und ein paar Meta-Daten (z.B. der letzte Abruf) werden im Arbeitsspeicher abgelegt.

Zugegeben: Eine wirklich stark vereinfachte Darstellung des Cachings mit nginx

Mit fastcgi_cache_path legst du also den eigentlichen Cache-Ordner fest. Danach deaktivierst du mit use_temp_path=off die Zwischenspeicherung in einem temporären Ordner, um den Cache-Prozess zu beschleunigen. Mit levels kannst du die Tiefe des Cache-Ordners festlegen. Jede Position steht zwischen den Doppelpunkten für ein Level, drei Level sind möglich. Die Ziffer legt fest, wieviel Zeichen die Dateinamen enthalten. Folgende Angabe reduziert die Tiefe z.B. auf 2 Level deren Ordnernamen 1 Zeichen enthalten:

Der Parameter keys_zone gibt dem Bereich im Arbeitsspeicher einen eindeutigen Namen, der das “Cache-Verzeichnis” enthält. Das ist notwendig, da du auch andere Cache-Bereich anlegen kannst (z.b. den proxy_cache). Die Ziffer hinter dem Doppelpunkt gibt die Größe der Liste an. 1 MByte entspricht etwa 8.000 cache keys – mit 100 MB solltest du also eine Weile auskommen.

Mit inactive=60m legst du fest, wie lange ein Objekt im Cache gültig ist, in diesem Fall 60 Minuten. Wenn du mit Inhalten arbeitest, die sich sehr oft ändern, solltest du diesen Wert natürlich verkleinern. Schließlich kannst du mit max_size die tatsächlicheGröße des Caches im Dateisystem begrenzen.

Die Direktive fastcgi_cache_path wird nicht auf Server-Ebene angegeben, sondern global unter http. Du kannst damit beliebig viele Caches anlegen, musst aber unbedingt auf die Unterscheidbarkeit achten, damit nginx die Caches deiner unterschiedlichen Server nicht zusammenhaut. Wie macht sich das bemerkbar? Wenn du eine deiner Seiten lädst (www.example.com) und plötzlich auf einer völlig anderen deiner Seiten (Domain) landest (www.test.com), solltest du dir die Direktiven fastcgi_cache_path oder fastcgi_cache_key noch mal genauer anschauen.

Die PHP-Einstellungen

Jetzt wird es spannend um nicht zu sagen: etwas kompliziert. Die Einstellungen für den PHP-Interpreter in fastcgi-php.conf. Diese bezieht sich alleine auf Dateien, deren Dateiendung ich in location festlege. Zunächst nutzen wir ein paar Standard-Einstellungen aus der bei nginx mitgelieferten fastcgi.conf-Datei. Hier werden einige Werte festgelegt, wie z.B. Document Root, Protokolle usw. Das muss zwingend zu Beginn passieren, da wir einige Parameter weiter unten überschreiben. Außerdem wird noch die Standard-Script-Datei festgelegt, sollte keine Datei in der URL mitgegeben werden.

Mit fastcgi_cache verweise ich nun auf Cache-Zone, die ich oben bereits definiert habe. Hier kannst du mit Parameter arbeiten ($server). Mit fastcgi_cache_valid kann ich für jeden HTTP-Antwortcode festlegen, wie lange der Cache gültig ist. Ich verweise hier nur auf erfolgreiche Anfragen (HTTP 200). Weiter oben habe ich bereits festgelegt, welche Anfragen überhaupt gecached werden, hier kann ich diese Anfragen mit fastcgi_cache_bypass nun explizit ausklammern.

Danach folgt eine wordpress-exklusive Einstellung: Die PHP-Datei wird nur an den PHP-Interpreter weitergereicht, wenn sie sich nicht im Ordner “uploads” befinden. Das ist ein Sicherheitsfeature: Sollte irgendwie eine PHP-Datei mit schadhaften Code in den (üblicherweise) schreibbaren Ordner “Uploads” gelangen, wird nginx diesen niemals an PHP weitergeben.

Weiter geht es mit einer wichtigen Einstellung für Sicherheit und Geschwindigkeit. Wir haben oben zwar schon grob festgelegt, welche Dateien nicht als Script zu PHP geschickt werden. Das ist aber noch ziemlich wacklig (warum, das ist hier ganz gut beschrieben): Was wir bisher nicht vermeiden, ist der Aufruf von z.B. /test.jpg/index.php – die Datei index.php würde vom Interpreter nicht gefunden werden. Er würde demnach versuchen, test.jpg auszuführen und den Anhang als Parameter verstehen. Das wollen wir vermeiden.

Es gibt viele Möglichkeiten, das zu verhindern. Einige davon werden wir hier nutzen.

Mit fastcgi_split_path_info zerlegst du die URL in den Pfad und den Dateinamen um zielsicher zu erkennen, welcher Teil der URL auf eine Datei zeigt und was als Ordner verstanden wird. Die RegExe beinhaltet deswegen zwei Capture-Gruppen. Der Inhalt der ersten Gruppe (.+.php) wird in der Variable $fastcgi_script_name abgelegt, der der zweiten Gruppe (/.+) landet in $fastcgi_path_info.

Mit try_files bestimmst du nun, dass nur PHP-Dateien verarbeitet werden, die überhaupt exisiterien. Das Problem dabei ist, dass dadurch der Parameter $fastcgi_path_info zurückgesetzt wird (siehe auch hier). Deshalb wird dessen Inhalt einen Schritt davor in die Variable $path_info geschrieben. Danach legen die Parameter für FastCGI fest und greifen nun auf die eben per RegExe extrahierten Infos für das Script und den Pfad zurück:

Damit das ganze wirklich reibungslos funktioniert, musst du in der php.ini den Parameter cgi.fix_pathinfo auf 1 setzen – das ist zwar die Standardeinstellungen, schau aber trotzdem noch mal nach:

Die HTTPS-Einstellungen

Die nächsten Parameter sind wieder etwas unkompliziert und auch selbsterklärend. Wir kommen zu den HTTP- und HTTPS-Einstellungen, die ich in einer Datei zusammengefasst habe (default_https.conf). Hier werden nur die Port-Einstellungen festgelegt, SSL korrekt eingerichtet und auf eine Standard-Datei verwiesen, wenn die Anfrage nicht auf eine Datei verweist:

Die GZIP-Einstellungen

Auch die Datei gzip.conf bedarf keiner großen Erklärung. Einen Großteil habe ich bereits global konfiguriert, hier werden auf Server-Ebene noch einige Einstellungen vorgenommen. Dabei setze ich das Kompressions-Level auf 3 und lege fest, welche Ressourcen komprimiert werden. Welches Level du wählst, hängt von deiner Hardware ab. Die Kompression kann die Auslieferung deiner Seite auf jeden Fall beschleunigen, einen etwas ausführlicheren Beitrag dazu findest du bei pingdom.com.

Die WordPress-Einstellungen

Weiter geht es mit der Datei wordpress.conf und noch ein paar Sicherheitsfeatures:

Wie du siehst beziehe ich mir hier erneut auf die Rate Limit Einstellung aus dem ersten Teil in der Datei nginx.conf. Mit Verweis auf meine Zone (one) beschränke ich die Warteschlange auf 10: burst=10; Wenn als mehr Anfragen als erlaubt ankommen (ich hatte 5 pro Sekunde zugelassen), werden bis zu 10 der darauf folgenden Anfragen in eine Warteschlange gepackt. Die anderen Parameter habe ich inline erklärt.

Die Logging-Einstellungen

Auf zur Datei logging.conf. Diese Einstellungen betreffen nicht nur das Log-Verhalten ansich, sondern haben auch Auswirkungen auf Geschwindigkeit und Sicherheit. Ich lege nämlich fest, welche Anfragen nicht ins Log-File geschrieben werden bzw. gänzlich ignoriert werden. Eine aus führliche Dokumentation findest du auf diesem Blog. Die Einträge sind inline beschrieben und erklären sich, so blöd das klingt, eigentlich selber. Nicht jeder Aufruf muss auch im Log dokumentiert werden. Uns interessieren ja eigentlich nur Fehler oder ungewöhnliche Anfragen.

Eine Sitemap korrekt einbinden

Die Einstellungen im Snippet sitemap.conf kommen ein wenig den berühmten “Kanonen auf Spatzen” gleich. Im Grunde bilde ich eine ganze Reihe von Spezialfällen ab, die beim Betrieb von WordPress und Sitemaps auftreten. Du kannst hier sicher einige Zeilen auslassen oder die Datei ganz ignorieren, wenn du ein komplett anderes Setup nutzt:

Sicherheits-Features

Zum Abschluss will ich noch ein paar Sicherheitsfeatures implementieren. In der Datei safety.conf passiert nicht viel, außer dass ich den Zugriff auf bestimmte kritische Dateien verbiete. Einiges davon bezieht sich explizit auf eine WordPress-Installation. Was du aus diesen Einstellungen mitnehmen solltest, ist die Info, wie du mit location, einer RegExe und deny all den Zugriff auf bestimmte Ressourcen verbietest.

Natürlich wollen wir nicht nur den Zugriff auf kritische Ressourcen verhindern, sondern ggf. auch andere Angriffsvektoren erschweren, wie z.B. BruteForce-Attacken. In einer WordPress-Installation ist ein beliebter Angriffspunkt z.B. die Datei wp-login.php. Weiter oben haben wir schon mal festgelegt, wie oft eine Ressouce abgefragt werden kann. Für wp-login.php wollen wir diese Grenze noch etwa enger ziehen. Unser Setup erlaubt 5 Anfragen / Sekunde. Mit Burst verkürze ich zuerst die Warteschlange auf 1. Mit nodelay sorge ich nun dafür, dass Anfragen sofort beantwortet werden, aber der Slot in de Warteschlange nicht gleich wieder frei wird. Ergo werden direkt darauf folgende Zugriffe im erlaubten Zeitfenster mit dem HTTP-Fehler 503 (Service temporarly not available) abgelehnt.

Weiter gehts abschließend mit der Einrichtung von PHP in Teil 3.

Schreibe einen Kommentar