# Problemstellung: Layer-Trennung (L3 vs. L7) ## Layer 7 (Application Layer) – Backend Fail2Ban arbeitet auf Layer 7: * **Analysiert:** HTTP-Requests (Logs) * **Erkennt:** Muster (Bots, Scans, SQL-Injection) * **Entscheidet:** „Bösartig / nicht bösartig“ Dieser Prozess erfolgt auf dem Backend (**clientserver**), da nur dort: * Vollständige Request-Daten vorliegen. * Die Applikationslogik sichtbar ist. --- ## Layer 3 (Network Layer) – Proxy Das eigentliche Blocking erfolgt auf Layer 3/4: * **IP-Adresse** wird geblockt. * **Protokollunabhängig.** * **Umsetzung:** Via `iptables`/`ipset` (Kernel). Dieser Prozess erfolgt optimal auf dem Proxy (**proxyserver**), da: * Dies der erste Eintrittspunkt in das Netzwerk ist. * Der Traffic dort noch nicht verteilt wurde. --- ## Technisches Problem ohne Trennung Wenn das Blocking ausschließlich auf dem Backend erfolgt: 1. Paket trifft auf dem Proxy ein. 2. Proxy akzeptiert die Verbindung. 3. Proxy leitet den Request an das Backend weiter. 4. Backend erkennt den Angriff (die Client-IP steht zwar in der Log, aber der eigentliche Verbindungspeer des Backends ist der Proxy). 5. Ein lokaler Block auf dem Backend ist wirkungslos: In der Firewall des Backends ist die Client-IP nie der Quelladresse – der gesamte Traffic kommt von der Proxy-IP. Würde man stattdessen die Proxy-IP bannen, wären **alle** Clients gleichzeitig blockiert. **Daraus resultierende Probleme:** * Der Proxy verarbeitet weiterhin Requests des Angreifers. * Verbindungen werden trotzdem aufgebaut. * Last entsteht bereits vor dem eigentlichen Block. > **Konsequenz:** Die Layer 7 Entscheidung erfolgt zu spät im Netzwerkpfad. --- ## Lösung: Layer-Trennung ### Architektur * **Layer 7 (Erkennung):** Backend * **Layer 3 (Blockierung):** Proxy ### Ablauf mit Proxy-Blocking 1. Angreifer sendet Request. 2. Proxy leitet diesen initial weiter. 3. Backend erkennt den Angriff via Fail2Ban. 4. Backend meldet die IP an den Proxy. 5. Proxy trägt die IP in die Firewall ein. 6. Alle weiteren Pakete werden bereits am Proxy verworfen. ### Effekt * Kein Durchreichen mehr zum Backend. * Keine HTTP-Verarbeitung für geblockte IPs. * Minimierung der Applikationslast. * Zentrale Kontrolle am Gateway. --- ## Technischer Vergleich | Ebene | Aufgabe | Ort | | :--- | :--- | :--- | | **Layer 7** | Analyse (Regex, Logs) | Backend | | **Layer 3/4** | Blocking (IP) | Proxy | ### Notwendigkeit der Trennung * **Fail2Ban** benötigt Kontext (L7), der nur im Backend verfügbar ist. * **Firewall** benötigt Performance (L3), was nur am Einstiegspunkt effizient ist. **Diese Kombination liefert:** * Hohe Erkennungsqualität. * Minimale Systemlast. --- ## Fazit Blocking auf dem Backend ist funktional, aber ineffizient. Blocking auf dem Proxy ist effizient, benötigt jedoch eine externe Entscheidungslogik. **Die Lösung:** Erkennung auf Layer 7 (Backend) und Durchsetzung auf Layer 3 (Proxy). --- # Fail2Ban: Proxy Forwarding via rsyslog (Client → Proxy) Diese Dokumentation beschreibt die aktuelle Implementation (Stand: Okt 2026). Die frühere Lösung – pro Ban eine SSH-Verbindung vom Client zum Proxy mit `fail2ban-client set recidive banip ` – wurde verworfen: * **SSH-Storms beim fail2ban-Neustart:** Beim Restart feuern alle Jails ihre „Restore Ban“-Listen nach → dutzende SSH-Sessionen in wenigen Sekunden. * **Lockout-Risiko:** Der Proxy bannt die IP auf *allen* Ports (inkl. 22) → der SSH-Transport blockiert sich selbst. * **Skalierung:** Bei mehreren Backend-VMs summiert sich der SSH-Aufwand. Die neue Lösung nutzt **keinen SSH-Zugriff mehr**: Ban-/Unban-Ereignisse werden aus dem fail2ban-Log per **rsyslog (UDP 514)** an den Proxy gesendet, wo ein einzelnes Jail die Regeln in die Firewall schreibt. ## 1. Architektur ``` clientserver (L7 – Erkennung) proxyserver (L3 – Enforcement) ───────────────────────────── ────────────────────────────── fail2ban-Jails (Apache, sshd, ...) → /var/log/fail2ban.log → rsyslog imfile → UDP 514 ──────────────────────────────────► rsyslog imudp → /var/log/f2bexport.log → fail2ban-Jail „remote-bans“ → iptables/ipset DROP (allports) Unban-Zeilen: f2b-unban.sh (systemd-Service, tail auf /var/log/f2bexport.log) → fail2ban-client set remote-bans unbanip ``` Merkmale: * **Kein SSH**, keine Verbindung pro Ban – nur Log-Forwarding. * **Ein einziges Jail** auf dem Proxy, egal wieviele Jails/VMs dahinter liegen. * `banaction = …-allports` → der Proxy selbst ist auf allen Ports geschützt (inkl. SSH), ohne dass normale Nutzer tangiert werden. * **Unban-Ereignisse** werden gespiegelt, die Ban-Liste bleibt synchron. --- ## 2. Backend (clientserver / Apache) ### 2.1 mod_remoteip (echte Client-IP in den Logs) Ohne mod_remoteip loggt Apache die Proxy-IP als Requester → fail2ban würde die Proxy-IP bannen → der Proxy bannt sich selbst (alle Upstreams down → 504/502). Deshalb muss Apache die Client-IP aus `X-Forwarded-For` nehmen: **Datei:** `/etc/apache2/conf-available/remoteip.conf` (siehe `remoteip.conf` in diesem Repo) ```apache RemoteIPHeader X-Forwarded-For # Vertrauen zu Proxy (Public IPv6 & internes IPv4) RemoteIPInternalProxy 2a01:4f8:130:516e::160 RemoteIPInternalProxy 10.8.1.1 # HTTPS-Status für PHP (verhindert Redirect-Loops) SetEnvIf X-Forwarded-Proto "https" HTTPS=on ``` ```bash a2enmod remoteip a2enconf remoteip systemctl restart apache2 ``` **Check:** In der Apache-Accesslog muss die echte Client-IP stehen, nicht die Proxy-IP. ### 2.2 fail2ban * Normale Jails, **keine Custom-Actions** nötig. * Die `NOTICE … Ban ` / `NOTICE … Unban `-Zeilen in `/var/log/fail2ban.log` sind die einzige Schnittstelle zum Proxy. * Alte „forward“-Actions (SSH-basiert) entfernen oder auf `/bin/true` entschärfen. ### 2.3 rsyslog-Forwarding **Datei:** `/etc/rsyslog.d/40-to-proxy.conf` ``` module(load="imfile") input(type="imfile" File="/var/log/fail2ban.log" Tag="f2bexport") if $programname == "f2bexport" then { action(type="omfwd" target="2a01:4f8:130:516e::160" port="514" protocol="udp") stop } ``` ```bash systemctl restart rsyslog ``` **Hinweis:** Beim ersten Start liest `imfile` die Datei komplett von Anfang → die gesamte Ban-Historie wird einmalig gesendet → der Proxy baut damit sofort den vollständigen Spiegel der aktuellen Ban-Liste auf. --- ## 3. Proxy (proxyserver) ### 3.1 rsyslog-Empfang **Datei:** `/etc/rsyslog.d/40-f2bexport.conf` ``` module(load="imudp") input(type="imudp" port="514") if $programname == "f2bexport" then { action(type="omfile" file="/var/log/f2bexport.log") stop } ``` ```bash systemctl restart rsyslog ``` Test: Vom Client `logger -n -P 514 -t fail2ban "TEST"` → Zeile muss in `/var/log/f2bexport.log` landen. ### 3.2 Firewall (Proxmox) UDP 514 (v4 + v6) von der Client-VM zum Proxy erlauben. ### 3.3 fail2ban-Jail „remote-bans“ > **Achtung (fail2ban ≥ 1.0):** Die Host-Gruppe im Regex heißt > `fid` bzw. man verwendet den Platzhalter ``. > `(?P…)` wird in 1.0.x abgelehnt („No failure-id group“). **Filter:** `/etc/fail2ban/filter.d/remote-bans.conf` ``` [Definition] failregex = ^.*fail2ban\.actions\s+\[\d+\]:\s+NOTICE\s+\[[^\]]+\] (?:Restore )?Ban \s*$ ignoreregex = ^.*fail2ban\.actions\s+\[\d+\]:\s+NOTICE\s+\[[^\]]+\] Unban \s*$ ``` * `failregex` erfasst `Ban` **und** `Restore Ban` (Neustart des Client). * Unban-Zeilen werden vom Jail ignoriert (fail2ban hat kein `unbanregex`); das eigentliche Unbanning erledigt der Watcher aus Abschnitt 4. **Jail:** `/etc/fail2ban/jail.d/remote-bans.local` ``` [remote-bans] enabled = true backend = polling logpath = /var/log/f2bexport.log maxretry = 1 findtime = 86400 bantime = 604800 banaction = iptables-ipset-proto6-allports ignoreip = 127.0.0.1/8 ::1 10.8.0.0/16 46.4.5.60 2a01:4f8:130:516e::160 2a01:4f8:130:516e::55 88.133.6.20 2a05:fa40:17a:4b00::/64 ``` * `bantime` = das **Maximum** der Bantimes der Client-Jails (7 Tage). Früheres Unbanning auf dem Client kommt per Unban-Zeile trotzdem an. * `ignoreip` **muss** mindestens enthalten: die eigene Proxy-IP (v4+v6), die Client-/Proxy-IPs im internen Netz und die Admin-IPs – sonst bannt sich der Proxy selbst und alle Upstream-Verbindungen brechen ab (Symptom: 504/502 auf allen proxiedomains). --- ## 4. Unban-Watcher (Proxy) fail2ban kennt kein `unbanregex` → ein kleines systemd-Service tailt das Export-Log und unbant via `fail2ban-client`: **`/usr/local/bin/f2b-unban.sh`:** ```bash #!/usr/bin/env bash set -u LOG=/var/log/f2bexport.log touch "$LOG" tail -n0 -F "$LOG" | while IFS= read -r line; do [[ "$line" == *"Unban "* ]] || continue ip="${line##*Unban }" ip="${ip%%[[:space:]]*}" [[ -n "$ip" ]] || continue echo "unban: $ip" fail2ban-client set remote-bans unbanip "$ip" 2>/dev/null || true done ``` ```bash chmod +x /usr/local/bin/f2b-unban.sh ``` **`/etc/systemd/system/f2b-unban.service`:** ``` [Unit] Description=fail2ban remote unban watcher After=network.target fail2ban.service [Service] ExecStart=/usr/local/bin/f2b-unban.sh Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash systemctl daemon-reload systemctl enable --now f2b-unban ``` --- ## 5. Funktionstest ```bash # Client: fail2ban-client set sshd banip 1.2.3.4 # Proxy (nach wenigen Sekunden): fail2ban-client status remote-bans # → 1.2.3.4 in der Banned-Liste # Client: fail2ban-client set sshd unbanip 1.2.3.4 # Proxy: fail2ban-client status remote-bans # → IP wieder weg journalctl -u f2b-unban --no-pager | tail # → "unban: 1.2.3.4" ``` --- ## 6. Skalierung auf weitere VMs Pro zusätzlicher Backend-VM: 1. Backend: `mod_remoteip` (2.1) + rsyslog-Forward (2.3, Ziel = Proxy-IP). 2. Proxy: **nichts** – das bestehende `remote-bans`-Jail sammelt alle Ban-/Unban-Zeilen aller Clients in `/var/log/f2bexport.log`. Ein Jail, ein Log, ein Watcher – egal wieviele VMs melden. --- ## 7. Fehlerbilder / Lessons Learned | Symptom | Ursache | | :--- | :--- | | 504/502 auf **allen** Proxiedomains | Proxy hat sich selbst gebannt (eigene IP in `remote-bans`, allports) → Upstream-Timeouts. Abhilfe: Proxy-IP in `ignoreip`. | | SSH zum Proxy abgestürzt nach fail2ban-Start | Alter Mechanismus: „Restore Ban“-SSH-Storm + allports-Ban des SSH-Transports. Wird durch die rsyslog-Lösung beseitigt. | | `No failure-id group in '…'` | fail2ban ≥ 1.0: `(?P)` ungültig → `` (bzw. `(?P…)`). | | `interpolation key 'jailname' not valid` | fail2ban 1.0.2 erlaubt in Actions nicht alle Platzhalter (u. a. kein `jailname`). | | Jail findet 0 Treffer, `fail2ban-regex` matcht | Jail liest ab Dateiende (nur neue Zeilen); `fail2ban-regex` testet die ganze Datei. | | Bans erscheinen nicht | UDP 514 blockiert (PVE-Firewall) oder `programname`-Filter passt nicht (`f2bexport` vs. `fail2ban`). |