339 lines
11 KiB
Markdown
339 lines
11 KiB
Markdown
# 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 <ip>` – 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 <ip>
|
||
```
|
||
|
||
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 <ip>` / `NOTICE … Unban <ip>`-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 <proxy-ip> -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 `<HOST>`.
|
||
> `(?P<ip>…)` 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 <HOST>\s*$
|
||
ignoreregex = ^.*fail2ban\.actions\s+\[\d+\]:\s+NOTICE\s+\[[^\]]+\] Unban <HOST>\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<ip>)` ungültig → `<HOST>` (bzw. `(?P<fid>…)`). |
|
||
| `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`). |
|