Wordfence gehört zu den bekanntesten Sicherheitsplugins für WordPress. Seine Web Application Firewall (WAF) erkennt verdächtige Anfragen, blockiert bekannte Angriffsmuster und schützt WordPress unter anderem vor Brute-Force-Angriffen und automatisierten Scans.
Die Blockierung erfolgt dabei normalerweise innerhalb der WordPress – beziehungsweise innerhalb der PHP-Umgebung.
Mit einer zusätzlichen Integration in Fail2ban lassen sich wiederholt auffällige IP-Adressen bereits auf Netzwerkebene sperren. Die Firewall des Linux-Servers verwirft beziehungsweise verweigert dann weitere Verbindungen, bevor diese den Webserver und die WordPress-Anwendung erreichen.
In diesem Beitrag beschreibe ich, wie sich Wordfence-Blockierungsereignisse aus der WordPress-Datenbank auslesen, in eine eigene Logdatei exportieren und anschließend mit Fail2ban und nftables verarbeiten lassen.
Die vorgestellte Lösung funktioniert grundsätzlich mit Wordfence Free und Wordfence Premium, sofern die benötigten Ereignisse in der Datenbank verfügbar sind.
Warum Wordfence mit Fail2ban kombinieren?
Wordfence bietet bereits umfangreiche Schutzmechanismen. Allerdings unterscheidet sich die technische Ebene, auf der ein Angriff blockiert wird.
Wordfence:
- Erkennt Angriffe auf WordPress.
- Analysiert HTTP-Anfragen.
- Blockiert verdächtige Zugriffe über seine WAF.
- Protokolliert sicherheitsrelevante Ereignisse.
Fail2ban mit nftables:
- Überwacht Protokolldateien.
- Erkennt wiederholte Auffälligkeiten anhand definierter Filter.
- Sperrt Quell-IP-Adressen zeitweise.
- Setzt die Sperren direkt über die Linux-Firewall um.
Der Vorteil liegt in der Kombination beider Systeme. Wordfence übernimmt die anwendungsspezifische Angriffserkennung, während Fail2ban wiederholt auffällige IP-Adressen bereits auf Netzwerkebene blockiert. Dadurch können insbesondere automatisierte Angriffe reduziert werden, die andernfalls wiederholt WordPress beziehungsweise PHP erreichen würden. Die Integration ersetzt Wordfence nicht, sondern ergänzt dessen Schutzmechanismen.
Architektur der Lösung
Die technische Umsetzung besteht aus mehreren Komponenten:
HTTP/HTTPS-Anfrage
|
v
WordPress
|
v
Wordfence WAF
|
v
Wordfence-Datenbank
wp_wfHits
|
v
Exportskript (Bash)
|
v
/var/log/wordfence-fail2ban.log
|
v
Fail2ban
|
v
nftables
|
v
IP-Adresse sperren
Ein systemd-Timer startet das Exportskript ungefähr jede Minute. Das Skript liest neue Wordfence-Ereignisse aus der Datenbank und schreibt relevante Blockierungen in eine Logdatei. Fail2ban überwacht diese Datei und sperrt IP-Adressen, die innerhalb eines festgelegten Zeitraums mehrfach auffallen.
Für das folgende Beispiel gelten:
| Einstellung | Wert |
|---|---|
| Maximale Treffer | 3 |
| Erkennungszeitraum | 10 Minuten |
| Sperrdauer | 24 Stunden |
| Gesperrte Ports | 80 und 443 |
| Firewall | nftables |
| Exportintervall | ca. 1 Minute |
Diese Werte lassen sich später an die eigenen Anforderungen anpassen.
Voraussetzungen
Für diese Anleitung wird ein Linux-Server mit folgenden Komponenten vorausgesetzt:
- WordPress mit installiertem und aktiviertem Wordfence
- MariaDB oder MySQL
- WP-CLI
- Fail2ban
- nftables
- systemd
- Bash
Die Beispiele orientieren sich an Debian beziehungsweise Ubuntu. Fail2ban und nftables lassen sich beispielsweise so installieren:
sudo apt update
sudo apt install fail2ban nftables
WP-CLI muss ebenfalls verfügbar sein.
In diesem Beispiel verwenden wir folgenden Installationspfad:
/var/www/example.org
Der Pfad muss an die tatsächliche WordPress-Installation angepasst werden.
WP-CLI liegt unter:
/usr/local/bin/wp
Für die Datenbankabfragen verwenden wir den Benutzer www-data. Das funktioniert nur, wenn dieser Benutzer Zugriff auf die WordPress-Dateien und die erforderlichen Datenbankinformationen besitzt.
Die Wordfence-Datenbank untersuchen
Wordfence speichert verschiedene sicherheitsrelevante Informationen in eigenen Datenbanktabellen. Für unsere Integration ist insbesondere die Tabelle wfHits interessant. Der tatsächliche Tabellenname hängt vom WordPress-Datenbankpräfix ab.
Zunächst ermitteln wir dieses Präfix:
sudo -u www-data /usr/local/bin/wp \
--path=/var/www/example.org \
db prefix
Beispiel:
wp_
Die relevante Tabelle heißt dann:
wp_wfHits
Wichtig: Auf Linux-Systemen mit MariaDB oder MySQL kann die Groß- und Kleinschreibung von Tabellennamen relevant sein. Deshalb sollte man den tatsächlichen Namen überprüfen:
sudo -u www-data /usr/local/bin/wp \
--path=/var/www/example.org \
db query "
SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME LIKE '%wfHits';
"
Welche Ereignisse protokolliert Wordfence?
Die Tabelle enthält unter anderem eine Spalte action. Mit folgender Abfrage lassen sich die vorhandenen Ereignistypen untersuchen:
sudo -u www-data /usr/local/bin/wp \
--path=/var/www/example.org \
db query "
SELECT action, COUNT(*) AS anzahl
FROM wp_wfHits
GROUP BY action
ORDER BY anzahl DESC
LIMIT 20;
"
Typische Einträge sind:
blocked:waf
blocked:wordfence
logged:waf
Für Fail2ban interessieren uns insbesondere tatsächliche Blockierungsereignisse. In unserem Beispiel berücksichtigen wir:
blocked:waf
blocked:wordfence
blocked:wfsn
Die Kennung blocked:wfsn stammt aus einer bestehenden Wordfence-Integration und wird aus Kompatibilitätsgründen berücksichtigt. Ob sie auf der eigenen Installation vorkommt, sollte anhand der Datenbank geprüft werden.
Ereignisse wie logged:waf werden bewusst nicht als Fail2ban-Treffer verwendet, da eine lediglich protokollierte Anfrage nicht automatisch eine Blockierung rechtfertigt.
Wie speichert Wordfence IP-Adressen?
Ein weiterer wichtiger Punkt ist die Speicherung der IP-Adressen. Wordfence kann IP-Adressen in einem binären Format speichern. IPv4-Adressen können dabei als sogenannte IPv4-Mapped-IPv6-Adressen vorliegen.
Beispiel:
::ffff:192.0.2.10
Mit folgender SQL-Abfrage lässt sich die Speicherung überprüfen:
SELECT
HEX(IP) AS ip_hex,
INET6_NTOA(IP) AS ip_address,
action
FROM wp_wfHits
WHERE action LIKE 'blocked:%'
ORDER BY id DESC
LIMIT 10;
Die Beispieladresse 192.0.2.10 stammt aus einem für Dokumentationszwecke reservierten Adressbereich. Das Exportskript wird solche IPv4-Mapped-Adressen in gewöhnliche IPv4-Adressen umwandeln.
Einschränkung: Die Implementierung verarbeitet ausschließlich IPv4-Adressen in diesem Speicherformat. Native IPv6-Adressen werden bewusst noch nicht exportiert. Wer auch IPv6-Angreifer sperren möchte, muss die IP-Konvertierung und -Validierung entsprechend erweitern.
Exportskript für Wordfence erstellen
Wir benötigen ein Skript, das regelmäßig neue Wordfence-Blockierungsereignisse ausliest.
Dazu erstellen wir:
sudo nano /usr/local/sbin/wordfence-to-fail2ban
Inhalt:
#!/bin/bash
set -euo pipefail
WP=/usr/local/bin/wp
SITE=/var/www/example.org
LOG=/var/log/wordfence-fail2ban.log
STATE_DIR=/var/lib/wordfence-fail2ban
STATE="$STATE_DIR/last-id"
mkdir -p "$STATE_DIR"
query() {
runuser -u www-data -- "$WP" \
--path="$SITE" \
db query "$1" \
--batch --raw --skip-column-names
}
# Datenbankpräfix automatisch ermitteln
PREFIX=$(
runuser -u www-data -- "$WP" \
--path="$SITE" db prefix
)
[[ "$PREFIX" =~ ^[a-zA-Z0-9_]+$ ]] || exit 1
TABLE="${PREFIX}wfHits"
# Beim ersten Start nur die aktuelle ID speichern
if [[ ! -f "$STATE" ]]; then
last=$(query \
"SELECT COALESCE(MAX(id),0) FROM \`$TABLE\`;")
[[ "$last" =~ ^[0-9]+$ ]] || exit 1
printf '%s\n' "$last" > "$STATE"
exit 0
fi
last=$(cat "$STATE")
[[ "$last" =~ ^[0-9]+$ ]] || exit 1
# Neue Wordfence-Ereignisse auslesen
sql="
SELECT
id,
FLOOR(ctime),
CASE
WHEN LEFT(HEX(IP),24) =
'00000000000000000000FFFF'
THEN SUBSTRING_INDEX(INET6_NTOA(IP), ':', -1)
ELSE '-'
END,
action
FROM \`$TABLE\`
WHERE id > $last
ORDER BY id
LIMIT 5000;
"
rows=$(query "$sql")
newlast=$last
while IFS=$'\t' read -r id epoch ip action; do
[[ -n "${id:-}" ]] || continue
[[ "$id" =~ ^[0-9]+$ ]] || exit 1
case "$action" in
blocked:waf|blocked:wordfence|blocked:wfsn)
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] &&
[[ "$epoch" =~ ^[0-9]+$ ]]; then
timestamp=$(
date -d "@$epoch" \
'+%Y-%m-%d %H:%M:%S'
)
printf '%s WF-BLOCK %s %s\n' \
"$timestamp" "$ip" "$action" >> "$LOG"
fi
;;
esac
newlast=$id
done <<< "$rows"
# Verarbeitungsstand atomar aktualisieren
printf '%s\n' "$newlast" > "${STATE}.tmp"
mv -f "${STATE}.tmp" "$STATE"
Anschließend die Berechtigungen setzen:
sudo chown root:root /usr/local/sbin/wordfence-to-fail2ban
sudo chmod 750 /usr/local/sbin/wordfence-to-fail2ban
Warum verwenden wir eine State-Datei?
Das Skript speichert die zuletzt verarbeitete Wordfence-Ereignis-ID unter:
/var/lib/wordfence-fail2ban/last-id
Dadurch werden bereits verarbeitete Ereignisse beim nächsten Durchlauf nicht erneut exportiert. Das verhindert, dass ein einzelnes Wordfence-Ereignis durch wiederholte Datenbankabfragen mehrfach als Fail2ban-Treffer gezählt wird.
Warum werden vorhandene Ereignisse übersprungen?
Beim erstmaligen Start wird ausschließlich die höchste bereits vorhandene Ereignis-ID gespeichert. Historische Blockierungen werden nicht importiert. Das ist wichtig, weil ansonsten möglicherweise Tausende ältere Ereignisse auf einmal in die Fail2ban-Logdatei geschrieben würden.
Warum maximal 5.000 Datensätze?
Die Begrenzung verhindert, dass eine einzelne Abfrage unbegrenzt viele Ereignisse verarbeitet. Bei mehr als 5.000 neuen Einträgen werden die übrigen Datensätze bei späteren Durchläufen verarbeitet. Dabei ist zu beachten, dass es sich um eine einfache ID-basierte Verarbeitung handelt. In Umgebungen mit sehr hoher Ereignisrate oder besonderen Anforderungen an die lückenlose Verarbeitung sollte der Exportmechanismus um zusätzliche Prüfungen erweitert werden.
Logdatei und Verarbeitungsstand vorbereiten
Vor dem ersten Start erstellen wir die benötigten Dateien und Verzeichnisse.
sudo install -d -o root -g root -m 750 \
/var/lib/wordfence-fail2ban
sudo install -o root -g root -m 640 \
/dev/null /var/log/wordfence-fail2ban.log
Anschließend kann man das Skript erstmals ausführen:
sudo /usr/local/sbin/wordfence-to-fail2ban
Die erste Ausführung sollte ohne Fehlermeldung beendet werden.
Kontrolle:
sudo cat /var/lib/wordfence-fail2ban/last-id
Die Datei sollte eine numerische ID enthalten. Dass die Logdatei zu diesem Zeitpunkt noch leer ist, ist normal.
Systemd-Service einrichten
Das Exportskript soll automatisch ausgeführt werden. Dazu verwenden wir einen systemd-Service und einen Timer.
Zunächst erstellet man:
sudo nano /etc/systemd/system/wordfence-fail2ban.service
Inhalt:
[Unit]
Description=Export Wordfence blocks to Fail2ban log
After=network-online.target mariadb.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/wordfence-fail2ban.lock /usr/local/sbin/wordfence-to-fail2ban
Der Service verwendet flock, um parallele Ausführungen zu verhindern. Das ist wichtig, da das Skript einen persistenten Verarbeitungsstand aktualisiert. Type=oneshot bedeutet, dass der Service nur während der Ausführung des Skripts aktiv ist. Nach erfolgreichem Abschluss erscheint er normalerweise als inactive (dead). Das ist kein Fehler.
Hinweis: mariadb.service ist für Installationen mit lokalem MariaDB-Dienst vorgesehen. Bei einem externen Datenbankserver oder MySQL muss die Abhängigkeit entsprechend angepasst werden.
Systemd-Timer erstellen
Dann erstellt man:
sudo nano /etc/systemd/system/wordfence-fail2ban.timer
Inhalt:
[Unit]
Description=Poll Wordfence blocks every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
AccuracySec=10s
Persistent=true
[Install]
WantedBy=timers.target
Danach läd man die systemd-Konfiguration neu:
sudo systemctl daemon-reload
Und aktiviert den Timer:
sudo systemctl enable --now wordfence-fail2ban.timer
Kontrolle:
systemctl list-timers wordfence-fail2ban.timer
Außerdem:
sudo journalctl \
-u wordfence-fail2ban.service \
-n 20 --no-pager
Bei erfolgreicher Ausführung sollten keine Fehler auftreten.
Nach neuen Wordfence-Blockierungen erscheinen Logeinträge wie:
2026-11-07 12:00:01 WF-BLOCK 192.0.2.10 blocked:waf
2026-11-07 12:00:05 WF-BLOCK 192.0.2.10 blocked:waf
2026-11-07 12:00:12 WF-BLOCK 192.0.2.10 blocked:wordfence
Die dargestellten IP-Adressen und Zeitpunkte sind fiktiv.
Fail2ban-Filter erstellen
Nachdem Wordfence seine Blockierungen in eine Logdatei schreibt, benötigen wir einen passenden Fail2ban-Filter.
Datei:
/etc/fail2ban/filter.d/wordfence-blocks.conf
Inhalt:
[Definition]
datepattern = {^LN-BEG}%%Y-%%m-%%d %%H:%%M:%%S
failregex = ^\s*WF-BLOCK <HOST> blocked:(?:waf|wordfence|wfsn)$
ignoreregex =
Der Filter erkennt Logeinträge nach folgendem Muster:
YYYY-MM-DD HH:MM:SS WF-BLOCK IP-ADRESSE blocked:AKTION
Die IP-Adresse wird über den Fail2ban-Platzhalter <HOST> extrahiert.
Das datepattern sorgt dafür, dass Fail2ban den Zeitstempel am Zeilenanfang korrekt interpretiert.
Filter testen
Für einen ungefährlichen Funktionstest erstellen wir eine Testdatei:
printf '%s WF-BLOCK 192.0.2.10 blocked:waf\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" \
| sudo tee /tmp/wordfence-test.log
Danach:
sudo fail2ban-regex \
/tmp/wordfence-test.log \
/etc/fail2ban/filter.d/wordfence-blocks.conf
Erwartet wird:
Failregex: 1 total
Lines: 1 lines, 0 ignored, 1 matched, 0 missed
Mit fail2ban-regex testen wir ausschließlich die Erkennung. Dabei wird keine IP-Adresse gesperrt.
Anschließend die Testdatei entfernen:
sudo rm /tmp/wordfence-test.log
Fail2ban-Jail für Wordfence einrichten
Jetzt erstellt man das eigentliche Jail.
Datei: /etc/fail2ban/jail.d/wordfence-blocks.local
Inhalt:
[wordfence-blocks]
enabled = true
filter = wordfence-blocks
logpath = /var/log/wordfence-fail2ban.log
backend = auto
banaction = nftables[type=multiport]
port = http,https
protocol = tcp
maxretry = 3
findtime = 600
bantime = 86400
Die wichtigsten Parameter:
| Parameter | Bedeutung |
|---|---|
enabled = true | Jail aktivieren |
filter | Verwendeter Fail2ban-Filter |
logpath | Überwachte Wordfence-Logdatei |
banaction | Sperren über nftables |
port | HTTP |
