Wordfence mit Fail2ban und nftables kombinieren – Angreifer auf Netzwerkebene sperren   Vor kurzem aktualisiert!


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.

Wordfence mit Fail2ban und nftables kombinieren
KI-generiertes Bild: Wordfence mit Fail2ban und nftables kombinieren

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:

EinstellungWert
Maximale Treffer3
Erkennungszeitraum10 Minuten
Sperrdauer24 Stunden
Gesperrte Ports80 und 443
Firewallnftables
Exportintervallca. 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:

ParameterBedeutung
enabled = trueJail aktivieren
filterVerwendeter Fail2ban-Filter
logpathÜberwachte Wordfence-Logdatei
banactionSperren über nftables
portHTTP