Wenn die .user.ini nicht ausreicht
Wordfence gehört zu den bekannten Sicherheitsplugins für WordPress. Die integrierte Web Application Firewall (WAF) bietet bereits in der kostenlosen Version einen soliden Grundschutz.
Besonders interessant ist dabei der sogenannte erweiterte Schutz (Extended Protection). Damit wird die Wordfence-Firewall bereits vor dem eigentlichen WordPress-Code geladen und kann verdächtige Anfragen entsprechend früh erkennen und blockieren. Was in der Theorie unkompliziert klingt, kann bei einer Installation mit Apache und PHP-FPM allerdings etwas kniffliger werden. Bei einer meiner WordPress-Installationen zeigte Wordfence trotz scheinbar korrekter Konfiguration dauerhaft nur den „Grundlegenden WordPress-Schutz“ an. Die Ursache war überraschend: PHP berücksichtigte die Wordfence-Konfiguration im WordPress-Hauptverzeichnis, nicht aber im Unterverzeichnis wp-admin.
Hier beschreibe ich, wie ich das Problem eingegrenzt und schließlich gelöst habe.
Ausgangssituation: WordPress nach einer Login-Angriffswelle
Der betroffene Webserver läuft unter Debian mit Apache und PHP-FPM. WordPress verwendet einen eigenen PHP-FPM-Pool.
Zunächst gab es massive Performanceprobleme im WordPress-Backend. Die Anmeldung dauerte teilweise ungewöhnlich lange, während andere Seiten noch vergleichsweise schnell antworteten. Eine Analyse der Apache-Logs und des PHP-FPM-Status zeigte schließlich die Ursache: Ein automatisierter Angreifer schickte zahlreiche POST-Anfragen an:
/wp-login.php
Zeitweise waren nahezu alle verfügbaren PHP-FPM-Worker mit diesen Anfragen beschäftigt. Um die Situation zu stabilisieren, wurden die auffälligen IP-Adressen gesperrt und ein separates Fail2ban-Jail für wiederholte Loginversuche eingerichtet. Wordfence war während der Fehlersuche vorübergehend deaktiviert worden, um seinen möglichen Einfluss auf die PHP-Antwortzeiten zu untersuchen.
Nach der Stabilisierung des Servers wurde das Plugin wieder aktiviert. Die Website lief anschließend wieder schnell – allerdings fiel im Wordfence-Dashboard ein Problem auf.
Wordfence meldet nur grundlegenden Schutz
Obwohl die Wordfence-Firewall aktiv war, zeigte das Dashboard weiterhin:
Firewall-Status: Aktiviert und schützend
Schutzstufe: Grundlegender WordPress-Schutz
Wordfence bietet zwei Betriebsarten seiner Firewall.
Grundlegender WordPress-Schutz
Bei diesem Modus wird die Firewall während des normalen WordPress-Ladevorgangs initialisiert. WordPress muss also bereits mit der Verarbeitung einer Anfrage begonnen haben, bevor die WAF vollständig eingebunden ist.
Erweiterter Schutz (Extended Protection)
Im erweiterten Modus wird die Firewall direkt über die PHP-Konfiguration eingebunden. Dafür wird die PHP-Direktive auto_prepend_file verwendet:
auto_prepend_file = '/var/www/example.org/wordfence-waf.php'
PHP lädt die angegebene Datei, bevor das eigentlich angeforderte PHP-Skript ausgeführt wird. Damit kann Wordfence Anfragen bereits vor dem Laden von WordPress prüfen. Gerade nach den zuvor beobachteten Login-Angriffen wollte ich sicherstellen, dass diese zusätzliche Schutzstufe tatsächlich aktiv ist.
Die .user.ini sah korrekt aus
Wordfence verwendet bei Apache mit PHP-FPM häufig eine Datei namens .user.ini.
Diese lag im WordPress-Hauptverzeichnis und enthielt bereits den erwarteten Eintrag:
; Wordfence WAF
auto_prepend_file = '/var/www/example.org/wordfence-waf.php'
; END Wordfence WAF
Auch die referenzierte Datei wordfence-waf.php war vorhanden.
Trotzdem meldete die Wordfence-Diagnose:
WAF auto prepend aktiv: Nein
Der Wordfence-Assistent zur Firewall-Optimierung wurde daraufhin erneut ausgeführt.
Als Servertyp wurde Apache + CGI/FastCGI ausgewählt.
Der Assistent bestätigte zwar eine erfolgreiche Installation, aber die Schutzstufe blieb unverändert.
Also musste die tatsächliche PHP-Konfiguration genauer untersucht werden.
Verwendet PHP-FPM überhaupt die .user.ini ?
Zunächst überprüfte ich die PHP-FPM-Konfiguration:
grep -RniE 'auto_prepend_file|user_ini' \
/etc/php/8.5/fpm/php.ini \
/etc/php/8.5/fpm/pool.d/
Die relevanten Einstellungen waren unauffällig:
;user_ini.filename = ".user.ini"
;user_ini.cache_ttl = 300
auto_prepend_file =
Die auskommentierten Direktiven entsprechen den üblichen Standardeinstellungen. Es gab auch keine erkennbare globale PHP-FPM-Einstellung, die auto_prepend_file überschrieben hätte. Interessant wurde es erst, als ich die tatsächlich verwendete Konfiguration direkt über Apache abfragte. Dazu erstellte ich vorübergehend eine kleine PHP-Testdatei:
<?php
header('Content-Type: text/plain');
echo 'SAPI: ', PHP_SAPI, "\n";
echo 'auto_prepend_file: ', ini_get('auto_prepend_file'), "\n";
echo 'user_ini.filename: ', ini_get('user_ini.filename'), "\n";
Im WordPress-Hauptverzeichnis lieferte der Aufruf:
SAPI: fpm-fcgi
auto_prepend_file: /var/www/example.org/wordfence-waf.php
user_ini.filename: .user.ini
Das war zunächst überraschend.
PHP-FPM verwendete die Wordfence-Konfiguration also durchaus korrekt.
Warum erkannte Wordfence den erweiterten Schutz trotzdem nicht?
Der entscheidende Unterschied: wp-admin
Als Nächstes führte ich denselben Test im Unterverzeichnis wp-admin durch.
Dort sah das Ergebnis plötzlich anders aus:
SAPI: fpm-fcgi
auto_prepend_file:
WAF file exists: yes
Die Wordfence-WAF-Datei war vorhanden, aber auto_prepend_file war leer.
Damit war der entscheidende Unterschied gefunden:
| Verzeichnis | auto_prepend_file |
|---|---|
| WordPress-Hauptverzeichnis | Korrekt gesetzt |
wp-admin | Leer |
Das erklärte auch, weshalb Wordfence seine frühzeitige Firewall-Einbindung nicht zuverlässig erkennen konnte. Eigentlich sollte PHP die .user.ini des Dokumentenstammverzeichnisses auch für darunterliegende Verzeichnisse berücksichtigen. In dieser Installation war das offensichtlich nicht der Fall.
Eine Suche nach zusätzlichen Konfigurationsdateien ergab ebenfalls nichts:
find . -name '.user.ini' -o -name 'php.ini'
Ergebnis:
./.user.ini
Es gab also keine weitere .user.ini in wp-admin, die den Wert überschrieben hätte.
Die Lösung: Wordfence direkt im PHP-FPM-Pool einbinden
Die WordPress-Installation verwendet einen eigenen PHP-FPM-Pool. Das hat den Vorteil, dass sich PHP-Einstellungen gezielt für diese Website konfigurieren lassen, ohne andere PHP-Anwendungen auf dem Server zu beeinflussen. Die Pool-Konfiguration befand sich unter:
/etc/php/8.5/fpm/pool.d/wordpress.conf
Vor der Änderung wurde eine Sicherung erstellt:
cp -a \
/etc/php/8.5/fpm/pool.d/wordpress.conf \
/etc/php/8.5/fpm/pool.d/wordpress.conf.bak-wordfence
Anschließend ergänzte ich folgende Direktive:
; Wordfence WAF - Extended Protection
php_admin_value[auto_prepend_file] = /var/www/example.org/wordfence-waf.php
Das ist der entscheidende Unterschied zur bisherigen Konfiguration.
Während eine .user.ini abhängig vom Verzeichnis ausgewertet wird, ist die Einstellung im PHP-FPM-Pool für die PHP-Prozesse dieses Pools maßgeblich.
Durch php_admin_value wird der Wert administrativ vorgegeben und kann nicht durch eine .user.ini überschrieben werden.
Anschließend wurde die PHP-FPM-Konfiguration geprüft:
php-fpm8.5 -t
Nach erfolgreichem Test:
systemctl reload php8.5-fpm
Damit war die WAF-Einbindung unabhängig von der zuvor beobachteten unterschiedlichen Verarbeitung der .user.ini konfiguriert.
Erfolg: Wordfence zeigt erweiterten Schutz
Nach dem Neuladen des Wordfence-Dashboards erschien endlich die gewünschte Meldung:
Schutzstufe: Erweiterter Schutz
Auch der allgemeine Firewall-Status lautete weiterhin:
Aktiviert und schützend
Die angezeigte Bewertung der Web Application Firewall stieg dabei von ungefähr 34 auf 54 Prozent. Diese Prozentwerte sind allerdings keine gemessenen Sicherheitswahrscheinlichkeiten. Wordfence berücksichtigt bei seiner Bewertung unter anderem Funktionen, die ausschließlich in der kostenpflichtigen Version verfügbar sind. Entscheidend war deshalb nicht der Prozentwert, sondern die bestätigte Aktivierung des erweiterten Schutzmodus.
Hat die Änderung Auswirkungen auf die Performance?
Nach der Umstellung überprüfte ich die Antwortzeiten der Website mehrfach mit curl:
for i in 1 2 3; do
curl -s -o /dev/null \
-w 'HTTP %{http_code} | Dauer %{time_total}s\n' \
https://example.org/
done
Die Ergebnisse:
HTTP 200 | Dauer 0.158123s
HTTP 200 | Dauer 0.095683s
HTTP 200 | Dauer 0.155855s
Die Startseite antwortete also weiterhin in etwa 100 bis 160 Millisekunden. Zusätzlich wurden die PHP-FPM-Slowlogs kontrolliert. Vereinzelt traten weiterhin Anfragen auf, die länger als die konfigurierte Slowlog-Schwelle von drei Sekunden benötigten. Betroffen waren unter anderem WordPress-Administrationsseiten und AJAX-Aufrufe. Es gab jedoch keinen Hinweis darauf, dass die Aktivierung des erweiterten Wordfence-Schutzes erneut zu einer dauerhaften Überlastung geführt hätte.
Wordfence und Fail2ban ergänzen sich
Eine wichtige Erkenntnis aus der ursprünglichen Performanceanalyse war, dass eine WordPress-WAF nicht jede Form von Überlastung allein verhindern muss. Bei massenhaften Loginversuchen ist es sinnvoll, zusätzlich möglichst früh auf Netzwerkebene einzugreifen. Auf dem betroffenen Server übernimmt deshalb ein separates Fail2ban-Jail die Erkennung wiederholter POST-Anfragen an:
/wp-login.php
Nach Überschreiten eines festgelegten Schwellenwerts wird die betreffende IP-Adresse über nftables gesperrt. Der Vorteil: Weitere Anfragen dieser IP erreichen PHP-FPM während der Sperrdauer nicht mehr. Wordfence und Fail2ban übernehmen damit unterschiedliche Aufgaben:
Fail2ban begrenzt wiederholte Angriffe anhand von Webserver-Logs und sperrt auffällige IP-Adressen auf Firewall-Ebene.
Wordfence untersucht HTTP-Anfragen aus Sicht der WordPress-Anwendung und schützt unter anderem vor bekannten Angriffsmustern.
Beide Mechanismen können sich sinnvoll ergänzen.
Was man bei PHP-FPM beachten sollte
Aus der Fehlersuche lassen sich einige praktische Erkenntnisse ableiten.
1. Eine korrekte .user.ini beweist noch nicht, dass die Einstellung überall greift
Entscheidend ist die tatsächlich verwendete PHP-Konfiguration bei einer Webanfrage.
Ein kurzer Test mit ini_get('auto_prepend_file') kann hier mehr Klarheit schaffen als ein Blick in die Konfigurationsdatei.
2. Unterschiedliche Verzeichnisse getrennt prüfen
Wenn Wordfence den erweiterten Schutz nicht erkennt, kann es sinnvoll sein, das WordPress-Hauptverzeichnis und wp-admin miteinander zu vergleichen. Genau dieser Vergleich führte in meinem Fall zum entscheidenden Hinweis.
3. Ein eigener PHP-FPM-Pool vereinfacht die Konfiguration
Wenn WordPress einen dedizierten PHP-FPM-Pool verwendet, lassen sich wichtige PHP-Einstellungen zentral festlegen. Die verwendete Lösung:
php_admin_value[auto_prepend_file] = /var/www/example.org/wordfence-waf.php
sollte allerdings nur für den passenden WordPress-Pool gesetzt werden. Ein gemeinsam genutzter Pool könnte sonst auch andere PHP-Anwendungen beeinflussen.
4. Änderungen immer testen
Vor einem PHP-FPM-Reload sollte die Konfiguration geprüft werden:
php-fpm8.5 -t
Ebenso sollten bei Apache-Änderungen zunächst Syntaxprüfungen erfolgen:
apache2ctl configtest
Und temporäre PHP-Diagnosedateien gehören nach dem Test sofort wieder gelöscht.
5. Sicherheit und Performance gemeinsam betrachten
Eine aktivierte Firewall allein sagt noch nichts über die Antwortzeiten einer Website aus.
Gerade nach Änderungen an PHP-FPM oder Sicherheitsplugins empfiehlt es sich, sowohl die normalen Antwortzeiten als auch die PHP-Slowlogs zu kontrollieren.
Das Problem war nicht eine defekte Wordfence-Installation.
Die Firewall war aktiviert und die .user.ini enthielt den korrekten Eintrag. Trotzdem funktionierte die frühzeitige WAF-Einbindung nicht in allen getesteten WordPress-Verzeichnissen.
Erst der Vergleich der tatsächlich verwendeten PHP-Einstellungen im Hauptverzeichnis und unter wp-admin brachte Klarheit.
Die Lösung bestand darin, Wordfence direkt über den dedizierten PHP-FPM-Pool einzubinden:
php_admin_value[auto_prepend_file] = /var/www/example.org/wordfence-waf.php
Danach erkannte Wordfence den erweiterten Schutz korrekt. Auch die Performance blieb mit Antwortzeiten von ungefähr 100 bis 160 Millisekunden unauffällig. Für mich zeigt dieser Fall einmal mehr, wie hilfreich es ist, bei Problemen nicht ausschließlich im Plugin-Dashboard zu suchen, sondern auch Apache, PHP-FPM und die tatsächlich wirksamen PHP-Einstellungen zu untersuchen. Manchmal liegt die entscheidende Ursache nicht im Plugin selbst, sondern eine Ebene darunter.
