VPN-Zugangskontrolle: Granularer Zugriff mit wg-easy Hooks und Firewall-Regeln
Du hast ein WireGuard-VPN mit wg-easy am Laufen und Familie oder Freunde bekommen Zugriff auf Plex und Jellyfin. Aber eigentlich willst du nicht, dass jeder VPN-Client dein gesamtes Netzwerk sehen kann – der Kumpel soll Filme schauen, nicht deinen Proxmox-Cluster erreichen. Die Lösung: ein mehrstufiges Sicherheitskonzept, bei dem vier Schichten zusammenarbeiten, um den Zugriff granular zu kontrollieren.
Falls du WireGuard und wg-easy noch nicht kennst: Im WireGuard-Grundlagen-Post findest du alles zum Einstieg, und im v15-Migrations-Post die Neuerungen der aktuellen Version.
Das Konzept: 4-Schichten-Sicherheit
Statt sich auf eine einzelne Maßnahme zu verlassen, bauen wir vier unabhängige Sicherheitsschichten auf. Selbst wenn eine Schicht versagt, fangen die anderen den Angriff ab:
flowchart TB
Client([VPN-Client<br>10.8.0.x]) --> L1
L1[Schicht 1<br>VPN-DMZ VLAN] --> L2
L2[Schicht 2<br>Router-Firewall] --> L3
L3[Schicht 3<br>wg-easy Hooks] --> L4
L4[Schicht 4<br>NPM Access Lists] --> Services
Services([Dienste<br>Plex, Jellyfin, Overseerr])
style L1 fill:#fef3eb,stroke:#f0883e
style L2 fill:#fcf7ec,stroke:#e3b341
style L3 fill:#e9fced,stroke:#27c93f
style L4 fill:#f3f9fb,stroke:#88c0d0
| Schicht | Was | Schützt gegen |
|---|---|---|
| 1: VPN-DMZ | Eigenes VLAN für den VPN-Server | Seitliche Bewegung ins LAN |
| 2: Router-FW | Inter-VLAN Regeln, Firewall-Gruppen | Zugriff auf nicht freigegebene VLANs |
| 3: wg-easy Hooks | iptables pro Client (Admin/Restricted) | Zugriff auf nicht freigegebene Dienste |
| 4: NPM Access Lists | deny/allow pro Proxy Host | Zugriff auf Admin-Interfaces über den Reverse Proxy |
Jede Schicht funktioniert unabhängig. Vergisst du eine iptables-Regel im Hook, blockiert die Router-Firewall trotzdem den Zugriff. Fehlt eine Firewall-Regel, greift die NPM Access List. Defense in Depth.
Schicht 1: VPN-DMZ – Netzwerk-Isolation
Der erste und wichtigste Schritt: Der WireGuard-Server gehört nicht ins Server-VLAN oder ins Default-LAN. Er bekommt ein eigenes, isoliertes Subnetz.
Warum ein eigenes VLAN?
Wenn wg-easy direkt im Server-VLAN steht, hat jeder VPN-Client automatisch Layer-2-Zugriff auf alle Server. ARP-Scanning, Broadcast-Sniffing – alles möglich. Ein eigenes VLAN erzwingt, dass sämtlicher Traffic durch die Router-Firewall muss.
Setup
Erstelle ein dediziertes VLAN auf deinem Router:
| Parameter | Beispielwert |
|---|---|
| VLAN-Name | VPN-DMZ |
| VLAN-ID | 45 |
| Subnetz | 192.168.45.0/24 |
| Gateway | 192.168.45.1 |
| DHCP | Optional (wg-easy bekommt eine statische IP) |
Die wg-easy VM bekommt eine statische IP in diesem VLAN, z.B. 192.168.45.10.
Die VPN-DMZ sollte kein Internet-Routing per Default haben. Der einzige erlaubte Traffic ist der WireGuard-Port (UDP) von außen und die explizit freigegebenen Verbindungen zum Server-VLAN.
Port Forwarding
Auf dem Router muss ein Port Forward eingerichtet werden, damit externe Clients den WireGuard-Server erreichen:
| Externer Port | Protokoll | Ziel-IP | Ziel-Port |
|---|---|---|---|
| 51820 | UDP | 192.168.45.10 | 51820 |
Schicht 2: Router-Firewall – Inter-VLAN Regeln
Die Router-Firewall kontrolliert, welche VLANs miteinander kommunizieren dürfen. Standardmäßig sollte kein Inter-VLAN Traffic erlaubt sein (Default Drop).
Firewall-Gruppen
Erstelle IP-Gruppen auf dem Router für die erlaubten Ziele:
| Gruppe | Mitglieder | Zweck |
|---|---|---|
| VPN-DMZ | 192.168.45.0/24 | Quell-VLAN für VPN-Traffic |
| VPN-SERVICES | 192.168.10.50, 192.168.10.51, 192.168.10.52, 192.168.10.100 | Erlaubte Ziel-Server |
Firewall-Regeln
In den LAN-IN Regeln (oder dem Äquivalent deines Routers):
| # | Aktion | Quelle | Ziel | Protokoll | Beschreibung |
|---|---|---|---|---|---|
| 1 | Allow | VPN-DMZ | VPN-SERVICES | TCP/UDP | VPN-Clients → erlaubte Dienste |
| 2 | Allow | VPN-SERVICES | VPN-DMZ | TCP/UDP (established) | Antwort-Traffic zurück |
| 3 | Drop | VPN-DMZ | RFC1918 | Alle | Alles andere blockieren |
Diese Regeln funktionieren auf jedem Router der Inter-VLAN Firewall unterstützt – UniFi, pfSense, OPNsense, MikroTik. Die Syntax unterscheidet sich, das Konzept bleibt gleich.
Ohne VLAN-fähigen Router
Falls dein Router keine VLANs unterstützt, kannst du die wg-easy VM in ein separates Subnetz stecken und statische Routen verwenden. Die Firewall-Regeln werden dann direkt auf dem WireGuard-Host (Schicht 3) umso wichtiger.
Schicht 3: wg-easy Hooks – Per-Client iptables-Regeln
Das Herzstück der Zugangskontrolle. wg-easy v15 unterstützt Per-Client Hooks – jeder Client kann eigene PostUp/PostDown-Regeln bekommen. Damit definierst du pro Person, was erlaubt ist.
Zwei Zugangsstufen
| Stufe | Für wen | Zugriff |
|---|---|---|
| Admin | Du selbst | Voller Zugriff auf alle VLANs und Services |
| Restricted | Familie, Freunde | Nur explizit freigegebene Dienste (Whitelist) |
Variablen in wg-easy v15
wg-easy stellt in den Hooks Platzhalter bereit:
| Variable | Bedeutung | Beispiel |
|---|---|---|
{{ipv4Cidr}} |
WireGuard-Subnetz | 10.8.0.0/24 |
{{device}} |
Netzwerk-Interface des Hosts | eth0 |
{{port}} |
WireGuard Listen-Port | 51820 |
Admin-Hook (unrestricted)
Für Admin-Clients: volle FORWARD-Erlaubnis in beide Richtungen.
PostUp:
1
2
3
iptables -A INPUT -p udp -m udp --dport -j ACCEPT
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -j ACCEPT
PostDown (spiegelbildlich mit -D statt -A):
1
2
3
iptables -D INPUT -p udp -m udp --dport -j ACCEPT
iptables -D FORWARD -i wg0 -j ACCEPT
iptables -D FORWARD -o wg0 -j ACCEPT
Statt MASQUERADE wird auf dem Router eine statische Route eingerichtet (
10.8.0.0/24 → 192.168.45.10). So bleiben die echten Client-IPs erhalten und NPM Access Lists können pro Client greifen. Ohne statische Route koennten die Zielserver die Antwortpakete nicht an die VPN-Client-IP (z.B.10.8.0.3) zurueckrouten.
Restricted-Hook (Whitelist)
Hier wird es interessant. Der Restricted-Hook arbeitet nach dem Whitelist-Prinzip:
- ESTABLISHED,RELATED – Antwortpakete erlauben (Stateful Firewall)
- ACCEPT pro Dienst – Einzelne Ziel-IP + Port Kombination erlauben
- DROP als Default – Alles andere wird geblockt
PostUp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Basis
iptables -A INPUT -p udp -m udp --dport -j ACCEPT
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -j ACCEPT
# Stateful Tracking (Antwortpakete)
iptables-nft -I FORWARD 1 -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Whitelist: Erlaubte Dienste
iptables-nft -I FORWARD 2 -i wg0 -d 192.168.10.50 -p tcp --dport 443 -j ACCEPT # NPM (Reverse Proxy)
iptables-nft -I FORWARD 3 -i wg0 -d 192.168.10.100 -p tcp --dport 32400 -j ACCEPT # Plex
iptables-nft -I FORWARD 4 -i wg0 -d 192.168.10.100 -p tcp --dport 8096 -j ACCEPT # Jellyfin
iptables-nft -I FORWARD 5 -i wg0 -d 192.168.10.100 -p tcp --dport 5055 -j ACCEPT # Overseerr
iptables-nft -I FORWARD 6 -i wg0 -d 192.168.10.51 -p udp --dport 53 -j ACCEPT # Pi-Hole DNS
iptables-nft -I FORWARD 7 -i wg0 -d 192.168.10.51 -p tcp --dport 53 -j ACCEPT # Pi-Hole DNS
iptables-nft -I FORWARD 8 -i wg0 -d 192.168.10.52 -p udp --dport 53 -j ACCEPT # Pi-Hole DNS (Backup)
iptables-nft -I FORWARD 9 -i wg0 -d 192.168.10.52 -p tcp --dport 53 -j ACCEPT # Pi-Hole DNS (Backup)
# Default: Alles andere blockieren
iptables-nft -I FORWARD 10 -i wg0 -j DROP
In wg-easy trägst du die Hooks als eine Zeile ein, getrennt durch
;. Die mehrzeilige Darstellung hier dient nur der Lesbarkeit.
PostDown (jede Regel mit -D statt -I / -A):
1
2
3
4
5
6
7
8
9
10
11
12
13
iptables -D INPUT -p udp -m udp --dport -j ACCEPT
iptables -D FORWARD -i wg0 -j ACCEPT
iptables -D FORWARD -o wg0 -j ACCEPT
iptables-nft -D FORWARD -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.50 -p tcp --dport 443 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.100 -p tcp --dport 32400 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.100 -p tcp --dport 8096 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.100 -p tcp --dport 5055 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.51 -p udp --dport 53 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.51 -p tcp --dport 53 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.52 -p udp --dport 53 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.52 -p tcp --dport 53 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -j DROP
Paketfluss für Restricted-Clients
flowchart LR
A[VPN-Client<br>10.8.0.3] -->|Paket an<br>192.168.10.100:32400| B[wg0]
B -->|Quell-IP bleibt<br>10.8.0.3| C{iptables<br>FORWARD}
C -->|ESTABLISHED?| D[Nein - neuer Flow]
D -->|Match Regel 3<br>Plex 32400/tcp| E[ACCEPT]
E --> F[Plex Server<br>192.168.10.100]
style A fill:#e1f5ff
style E fill:#e1ffe1
style F fill:#fff3e0
flowchart LR
A[VPN-Client<br>10.8.0.3] -->|Paket an<br>192.168.10.15:8006| B[wg0]
B --> C{iptables<br>FORWARD}
C -->|Kein Match<br>in Regeln 1-9| D[Regel 10:<br>DROP]
style A fill:#e1f5ff
style D fill:#ffe1e1
Schicht 4: NPM Access Lists – Reverse Proxy absichern
Die wg-easy Hooks kontrollieren, welche IPs und Ports erreichbar sind. Aber was ist mit Diensten, die hinter dem Reverse Proxy laufen? Wenn ein VPN-Client Port 443 des Reverse Proxy erreichen darf (weil dort Plex und Jellyfin laufen), kann er theoretisch auch jedes andere Proxy-Ziel aufrufen – inklusive Admin-Interfaces wie Proxmox, Portainer oder die Router-UI.
Hier kommen NPM Access Lists ins Spiel.
Access List erstellen
In Nginx Proxy Manager: Access Lists > Add Access List
| Feld | Wert |
|---|---|
| Name | Block VPN Clients |
| Satisfy Any | Ja |
Im Tab Access:
1
2
3
deny 10.10.10.0/24 # wg-easy Instanz 1 Subnetz
deny 10.8.0.0/24 # wg-easy Instanz 2 Subnetz
allow 0.0.0.0/0 # Alle anderen erlauben
Access List zuweisen
Die Access List wird den Proxy Hosts zugewiesen, die VPN-Clients nicht erreichen sollen:
| Proxy Host | Access List | Grund |
|---|---|---|
| proxmox.example.com | Block VPN Clients | Hypervisor-Admin |
| portainer.example.com | Block VPN Clients | Container-Admin |
| npm.example.com | Block VPN Clients | Reverse Proxy Admin |
| pihole.example.com | Block VPN Clients | DNS-Admin |
| router.example.com | Block VPN Clients | Router-Admin |
Proxy Hosts die VPN-Clients erreichen duerfen, bekommen keine Access List:
| Proxy Host | Access List | Grund |
|---|---|---|
| plex.example.com | Keine | Streaming erlaubt |
| jellyfin.example.com | Keine | Streaming erlaubt |
| overseerr.example.com | Keine | Medienwuensche erlaubt |
Admins die sich ueber VPN einloggen und trotzdem auf Admin-Interfaces zugreifen muessen, kannst du per
allow-Regel in der Access List ausnehmen:allow 10.8.0.2(spezifische Admin-VPN-IP).
Wie NPM die Quell-IP erkennt
Damit die Access Lists die VPN-Clients korrekt filtern koennen, muss NPM die echte Client-IP sehen. Der empfohlene Weg ist eine statische Route auf dem Router – alternativ gibt es den MASQUERADE-Ansatz als Fallback:
Ansatz A: Kein MASQUERADE (empfohlen)
Statt MASQUERADE richtest du eine statische Route auf dem Router ein:
| Subnetz | Gateway | Beschreibung |
|---|---|---|
| 10.8.0.0/24 | 192.168.45.10 | Route VPN-Traffic zum wg-easy Server |
Vorteil: NPM sieht die echte VPN-Client-IP (z.B. 10.8.0.3), Access Lists und Logs werden aussagekraeftiger.
Ansatz B: Mit MASQUERADE
Falls statische Routen nicht moeglich sind, blockierst du in der Access List die wg-easy Server-IP:
1
2
deny 192.168.45.10 # wg-easy Server IP
allow 0.0.0.0/0
Nachteil: Alle VPN-Clients werden gleich behandelt, keine Unterscheidung zwischen Admin und Restricted.
Services erweitern: Neuen Dienst freigeben
Dein Kumpel will jetzt auch Nextcloud ueber VPN nutzen, oder du richtest Audiobookshelf fuer Hoerbuecher ein. Jedes Mal sind bis zu drei Schichten betroffen. Hier die vollstaendige Anleitung am Beispiel von Nextcloud (192.168.10.200:443).
Schritt 1: Router-Firewall pruefen (Schicht 2)
Ist die IP des neuen Dienstes bereits in deiner VPN-SERVICES Firewall-Gruppe? Falls der Dienst auf einem Server laeuft, der schon in der Gruppe ist (z.B. gleiche IP wie Plex), kannst du diesen Schritt ueberspringen.
Falls nicht: Fuege die IP zur Firewall-Gruppe hinzu.
| Situation | Aktion |
|---|---|
| Dienst auf bekanntem Server (z.B. 192.168.10.100) | Nichts zu tun |
| Dienst auf neuem Server (z.B. 192.168.10.200) | IP zur VPN-SERVICES Gruppe hinzufuegen |
Schritt 2: wg-easy Hook anpassen (Schicht 3)
Das ist der wichtigste Schritt. Du fuegst eine neue ACCEPT-Regel in den Restricted-Hook ein – und zwar vor der DROP-Regel.
Vorher (DROP auf Position 10):
1
2
iptables-nft -I FORWARD 9 -i wg0 -d 192.168.10.52 -p tcp --dport 53 -j ACCEPT # Pi-Hole DNS (Backup)
iptables-nft -I FORWARD 10 -i wg0 -j DROP # Default DROP
Nachher (Nextcloud auf Position 10, DROP rueckt auf 11):
1
2
3
iptables-nft -I FORWARD 9 -i wg0 -d 192.168.10.52 -p tcp --dport 53 -j ACCEPT # Pi-Hole DNS (Backup)
iptables-nft -I FORWARD 10 -i wg0 -d 192.168.10.200 -p tcp --dport 443 -j ACCEPT # Nextcloud
iptables-nft -I FORWARD 11 -i wg0 -j DROP # Default DROP
Vergiss nicht, die gleiche Regel auch im PostDown mit -D hinzuzufuegen:
1
iptables-nft -D FORWARD -i wg0 -d 192.168.10.200 -p tcp --dport 443 -j ACCEPT
Die Positionsnummern (
-I FORWARD 10) bestimmen die Reihenfolge. Jede neue Regel erhoet die Position der DROP-Regel um 1. Wenn du mehrere Dienste gleichzeitig hinzufuegst, zaehle sorgfaeltig durch.
Schritt 3: NPM Access List pruefen (Schicht 4)
Falls der neue Dienst hinter dem Reverse Proxy laeuft:
| Situation | Aktion |
|---|---|
| Neuer Proxy Host fuer Nextcloud | Keine Access List zuweisen (VPN-Clients duerfen zugreifen) |
| Bestehender Proxy Host hat “Block VPN Clients” | Access List entfernen, damit VPN-Clients durchkommen |
| Dienst laeuft nicht hinter NPM (direkte IP) | Kein NPM-Schritt noetig |
Schritt 4: Testen
- VPN trennen und neu verbinden – die Hooks greifen nur beim Verbindungsaufbau
- Dienst aufrufen –
https://nextcloud.example.comoder direkthttps://192.168.10.200 - Admin-UIs gegenpruefen – stelle sicher, dass Proxmox, Portainer etc. weiterhin blockiert sind
1
2
3
4
5
6
# Auf dem wg-easy Host: Regeln pruefen
sudo iptables-nft -L FORWARD -n --line-numbers
# Erwartete Ausgabe (Ausschnitt):
# 10 ACCEPT tcp -- * wg0 0.0.0.0/0 192.168.10.200 tcp dpt:443
# 11 DROP all -- * wg0 0.0.0.0/0 0.0.0.0/0
Kurzreferenz: Typische Dienste
Hier eine Uebersicht gaengiger Self-Hosted Dienste und die noetige Konfiguration:
| Dienst | IP:Port | iptables-Regel | NPM Access List |
|---|---|---|---|
| Nextcloud | 192.168.10.200:443 | ACCEPT tcp dpt:443 | Keine (Zugriff erlaubt) |
| Audiobookshelf | 192.168.10.100:13378 | ACCEPT tcp dpt:13378 | Keine |
| Immich | 192.168.10.100:2283 | ACCEPT tcp dpt:2283 | Keine |
| Vaultwarden | 192.168.10.100:8080 | ACCEPT tcp dpt:8080 | Keine |
| Grafana | 192.168.10.100:3000 | ACCEPT tcp dpt:3000 | Block VPN Clients |
| Portainer | 192.168.10.100:9443 | Kein Eintrag (blockiert) | Block VPN Clients |
Dienste die sowohl fuer Restricted-Clients freigegeben als auch hinter NPM laufen, brauchen Eintraege in beiden Schichten: iptables-Regel UND NPM Proxy Host ohne Access List. Vergisst du eine Schicht, kommt der Client nicht durch.
Mehrere Dienste auf einmal hinzufuegen
Wenn du gleich drei oder vier Dienste freigibst, kann das Durchzaehlen der Regelpositionen fehleranfaellig werden. Ein Trick: Fuege alle neuen ACCEPT-Regeln mit aufsteigenden Positionsnummern ein und setze die DROP-Regel ganz am Ende.
1
2
3
4
5
# Drei neue Dienste: Nextcloud, Audiobookshelf, Immich
iptables-nft -I FORWARD 10 -i wg0 -d 192.168.10.200 -p tcp --dport 443 -j ACCEPT # Nextcloud
iptables-nft -I FORWARD 11 -i wg0 -d 192.168.10.100 -p tcp --dport 13378 -j ACCEPT # Audiobookshelf
iptables-nft -I FORWARD 12 -i wg0 -d 192.168.10.100 -p tcp --dport 2283 -j ACCEPT # Immich
iptables-nft -I FORWARD 13 -i wg0 -j DROP # Default DROP
Und die passenden PostDown-Regeln:
1
2
3
iptables-nft -D FORWARD -i wg0 -d 192.168.10.200 -p tcp --dport 443 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.100 -p tcp --dport 13378 -j ACCEPT
iptables-nft -D FORWARD -i wg0 -d 192.168.10.100 -p tcp --dport 2283 -j ACCEPT
Bonus: Selektives VPN-Routing ueber ProtonVPN
Optional kannst du bestimmten Traffic deiner VPN-Clients durch einen kommerziellen VPN-Provider (z.B. ProtonVPN) routen. Details zum kompletten Setup findest du im separaten Post: Selektives VPN-Routing: Bestimmte IPs ueber ProtonVPN tunneln.
Kurzzusammenfassung: Mit Policy-Based Routing (PBR) kannst du eine Liste von IPs/Domains definieren, deren Traffic ueber einen zweiten WireGuard-Tunnel (ProtonVPN) laeuft. Der Rest geht normal raus. So schuetzt du die Privatsphaere deiner Nutzer bei bestimmten Diensten, ohne den gesamten Traffic zu verlangsamen.
flowchart LR
A[VPN-Client] --> B[wg0]
B --> C{Ziel in<br>Route-Liste?}
C -->|Nein| D[Normales Routing<br>Heim-IP]
C -->|Ja| E[ProtonVPN-Tunnel<br>proton0]
E --> F[Internet<br>ProtonVPN-IP]
D --> G[Internet<br>Heim-IP]
style E fill:#f5ecfd,stroke:#bf5af2
style D fill:#e4f9e8,stroke:#27c93f
Wie PBR funktioniert
Auf dem wg-easy Host laeuft ein zweiter WireGuard-Tunnel (proton0) zum ProtonVPN-Server. Ein ipset enthaelt alle Ziel-IPs, die ueber diesen Tunnel geroutet werden sollen. Die Entscheidung faellt pro Paket in der iptables mangle-Chain:
- Paket kommt auf wg0 rein (vom VPN-Client)
- ipset-Lookup: Ist die Ziel-IP in der Route-Liste? (
ipset test vpn-routes <ip>) - Ja: Paket bekommt ein
fwmark(Markierung0x1) - IP Rule: Markierte Pakete nutzen Routing-Tabelle 100 statt der Main-Tabelle
- Tabelle 100: Default-Route zeigt auf
proton0→ Paket geht durch den ProtonVPN-Tunnel - MASQUERADE auf proton0: Quell-IP wird zur Tunnel-IP umgeschrieben
Das ipset-Lookup ist O(1) dank Hash-Tabelle – selbst mit hunderten Eintraegen gibt es keine spuerbare Latenz.
Die Route-Liste: routes.conf
Alle Ziele werden in einer einfachen Textdatei verwaltet:
1
2
3
4
5
6
7
8
9
10
# /opt/wg-easy/proton-pbr/routes.conf
# === Direkte IPs ===
178.17.166.194
185.195.236.11
103.214.7.28
# === Domains (werden automatisch aufgeloest) ===
# example-tracker.org
# streaming-service.net
Format:
- Eine IP oder Domain pro Zeile
- Kommentare mit
# - IPv4-Adressen werden direkt ins ipset uebernommen
- Domains werden per
dig +short <domain> Aaufgeloest – alle resultierenden IPs landen im ipset
Neue Ziele hinzufuegen
Eine IP hinzufuegen:
1
2
3
4
5
6
7
8
# 1. IP in die Route-Liste eintragen
echo "203.0.113.42" | sudo tee -a /opt/wg-easy/proton-pbr/routes.conf
# 2. Sofort anwenden (ohne auf den Cron-Job zu warten)
sudo /opt/wg-easy/proton-pbr/pbr-update.sh
# 3. Pruefen ob die IP im ipset ist
sudo ipset test vpn-routes 203.0.113.42
Eine Domain hinzufuegen:
1
2
3
4
5
6
7
8
9
# 1. Domain eintragen
echo "tracker-forum.example.org" | sudo tee -a /opt/wg-easy/proton-pbr/routes.conf
# 2. Anwenden -- das Script loest die Domain auf und fuegt alle IPs hinzu
sudo /opt/wg-easy/proton-pbr/pbr-update.sh
# 3. Pruefen welche IPs aufgeloest wurden
dig +short tracker-forum.example.org A
sudo ipset list vpn-routes
Domains sind praktisch fuer Dienste mit wechselnden IPs (CDNs, Load Balancer). Der Cron-Job loest sie alle 15 Minuten neu auf. Feste IPs sind zuverlaessiger und sofort aktiv.
Einen Eintrag entfernen:
1
2
3
4
5
# 1. Zeile aus routes.conf loeschen (z.B. mit sed oder Editor)
sudo sed -i '/203.0.113.42/d' /opt/wg-easy/proton-pbr/routes.conf
# 2. Update-Script laedt das ipset komplett neu
sudo /opt/wg-easy/proton-pbr/pbr-update.sh
Automatische Aktualisierung
Zwei Cron-Jobs sorgen dafuer, dass das PBR immer aktuell ist:
| Cron | Timing | Aufgabe |
|---|---|---|
@reboot sleep 30 && pbr-setup.sh |
Nach jedem Boot | Tunnel starten, ipset erstellen, iptables-Regeln setzen |
*/15 * * * * pbr-update.sh |
Alle 15 Minuten | Domains neu aufloesen, ipset aktualisieren |
Die 30 Sekunden Verzoegerung beim Boot gibt Docker und wg-easy Zeit zum Starten. Das 15-Minuten-Intervall fuer Updates ist ein guter Kompromiss: schnell genug fuer DNS-Aenderungen, ohne unnoetige Last.
Logs pruefen:
1
2
tail -f /var/log/pbr-setup.log # Boot-Log
tail -f /var/log/pbr-update.log # Update-Log
Verifizierung
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Tunnel aktiv?
sudo wg show proton0
# Exit-IP pruefen (sollte ProtonVPN-IP zeigen, nicht deine WAN-IP)
sudo curl --interface proton0 https://ifconfig.me
# Aktueller Inhalt des ipsets
sudo ipset list vpn-routes
# Routing-Tabelle 100 korrekt?
sudo ip route show table 100
# Erwartete Ausgabe: default dev proton0 scope link
# IP-Rule gesetzt?
sudo ip rule show | grep fwmark
# Erwartete Ausgabe: 100: from all fwmark 0x1 lookup 100
# Paketzaehler der mangle-Regel (sollte mit Traffic steigen)
sudo iptables -t mangle -L PREROUTING -n -v
PBR laesst sich nicht vom wg-easy Host selbst testen – nur Traffic der ueber das
wg0-Interface reinkommt, wird markiert. Teste immer von einem VPN-Client aus.
Alles zusammen: Komplettes Beispiel
Hier ein vollstaendiges Beispiel-Setup fuer einen WireGuard-Server mit wg-easy v15 und allen vier Sicherheitsschichten:
Netzwerk-Uebersicht
flowchart TB
Internet([Internet]) -->|Port 51820/UDP| Router[Router / Firewall<br>192.168.45.1]
Router -->|VLAN 45| WG[wg-easy Server<br>192.168.45.10<br>Subnet: 10.8.0.0/24]
Router -->|VLAN 60<br>Inter-VLAN FW| Server[Server-VLAN<br>192.168.10.0/24]
WG -.->|Admin Hook<br>ACCEPT all| Server
WG -.->|Restricted Hook<br>Whitelist only| NPM[NPM<br>192.168.10.50:443]
NPM --> Plex[Plex<br>:32400]
NPM --> Jellyfin[Jellyfin<br>:8096]
NPM --> Overseerr[Overseerr<br>:5055]
NPM -.->|Access List:<br>VPN blockiert| Proxmox[Proxmox<br>Admin-UI]
NPM -.->|Access List:<br>VPN blockiert| Portainer[Portainer<br>Admin-UI]
Server --> DNS[Pi-Hole DNS<br>:53]
style WG fill:#fcf0e3,stroke:#f0883e
style NPM fill:#edf7fa,stroke:#88c0d0
style Proxmox fill:#ffedeb,stroke:#ff5f56
style Portainer fill:#ffedeb,stroke:#ff5f56
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
hostname: wg-easy
network_mode: host
environment:
- PORT=51821
- WG_HOST=vpn.example.com
- WG_PATH=/
- DISABLE_IPV6=true
volumes:
- ./data:/etc/wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
restart: unless-stopped
wg-easy v15 mit
network_mode: hostbraucht Sysctls auf dem Host, nicht im Container. Erstelle/etc/sysctl.d/99-wireguard.confauf dem Docker-Host.
1
2
net.ipv4.ip_forward = 1
net.ipv4.conf.all.src_valid_mark = 1
1
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Checkliste fuer das Setup
- VLAN erstellen – Eigenes VPN-DMZ Subnetz auf dem Router
- VM/Container – wg-easy VM in der VPN-DMZ mit statischer IP
- Port Forward – WireGuard UDP-Port vom WAN zur wg-easy IP
- Firewall-Gruppen – VPN-DMZ und VPN-SERVICES Gruppen anlegen
- Inter-VLAN Regeln – Allow VPN-DMZ → Services, Drop VPN-DMZ → Rest
- Statische Route – VPN-Subnet → wg-easy IP (fuer NPM Access Lists)
- wg-easy einrichten – Container starten, Setup Wizard durchlaufen
- Admin-Hook – Fuer dich: voller Zugriff
- Restricted-Hook – Fuer Gaeste: nur Whitelist-Dienste
- NPM Access List – VPN-Subnets bei Admin-Proxy-Hosts blockieren
- DNS konfigurieren – VPN-Clients nutzen dein Pi-Hole als DNS
- Testen – Restricted-Client darf Plex erreichen, aber nicht Proxmox
Troubleshooting
VPN-Client kann nichts erreichen
1
2
3
4
5
# Auf dem wg-easy Host: Sind die iptables-Regeln aktiv?
sudo iptables-nft -L FORWARD -n --line-numbers
# Laeuft der WireGuard-Tunnel?
sudo wg show wg0
Pruefe die Reihenfolge: Wenn die DROP-Regel vor den ACCEPT-Regeln steht, wird alles blockiert. Die Regelnummern in -I FORWARD <NR> sind entscheidend.
Restricted-Client erreicht mehr als erlaubt
1
2
3
4
5
# Welche FORWARD-Regeln sind aktiv?
sudo iptables-nft -L FORWARD -n --line-numbers -v
# Gibt es eine ACCEPT-all Regel die zu frueht greift?
# -> Position pruefen, DROP muss die LETZTE Regel sein
NPM Access List greift nicht
- Pruefe ob der VPN-Client mit seiner echten IP ankommt:
1 2
# In den NPM Access Logs nach der Quell-IP schauen tail -f /data/logs/proxy-host-*_access.log | grep "10.8.0"
- Falls du die wg-easy Server-IP (z.B.
192.168.45.10) statt der Client-IP siehst, ist vermutlich MASQUERADE aktiv oder die statische Route auf dem Router fehlt. Pruefe die Route (VPN-Subnet → wg-easy IP) und stelle sicher, dass kein MASQUERADE in den wg-easy Hooks steht.
DNS funktioniert nicht ueber VPN
Im wg-easy Admin Panel: WireGuard > DNS Servers muss auf die Pi-Hole IPs zeigen (z.B. 192.168.10.51, 192.168.10.52). Diese IPs muessen auch in den iptables-Hooks erlaubt sein (Port 53 UDP + TCP).
Android-Nutzer aufgepasst: “Privates DNS” in den Android-Einstellungen muss auf Aus stehen. Sonst nutzt Android DNS-over-TLS und umgeht den VPN-Tunnel komplett.
Zusammenfassung
| Schicht | Werkzeug | Konfiguration | Granularitaet |
|---|---|---|---|
| 1: VPN-DMZ | Router VLAN | Eigenes Subnetz | Pro Netzwerk-Segment |
| 2: Router-FW | Firewall-Gruppen + Regeln | Inter-VLAN Allow/Drop | Pro VLAN/IP-Gruppe |
| 3: Hooks | wg-easy iptables | PostUp/PostDown pro Client | Pro Client + Dienst (IP:Port) |
| 4: NPM ACL | Access Lists | deny/allow pro Proxy Host | Pro Web-Dienst + Quell-IP |
Die Kombination dieser vier Schichten gibt dir die volle Kontrolle: Dein Kumpel schaut Filme auf Plex, deine Schwester nutzt Jellyfin, und keiner von beiden kann versehentlich (oder absichtlich) deinen Proxmox-Cluster oder die Router-UI erreichen. Und falls du doch mal jemandem mehr Rechte geben willst, aenderst du einfach den Hook von Restricted auf Admin.