Zum Hauptinhalt springen

Wireguard für eine Flotte von Raspi-road-warriors – und der Haken mit ufw

·1068 Wörter·6 min
KI-unterstützt
Kein Durchkommen

Für das erstmalige Einrichten eines Wireguard-VPN-Servers finden sich ja eine Vielzahl von Anleitungen im Internet, darum hier nur in aller Kürze. Das verwendete Setup sieht einen VPN-Server vor, mit dem alle Raspis als Clients verbinden. Als ersten Schritt installieren wir das wireguard Paket:

apt install wireguard

Anschließend die Datei /etc/wireguard/wg0.conf mit folgendem Inhalt anlegen:

[Interface]
Address = 10.10.100.5/24
SaveConfig = true
ListenPort = 51820

Dann wird ein private key erzeugt und dieser Konfigurationsdatei hinzugefügt:

{ echo -n "PrivateKey = " ; wg genkey; } | tee -a /etc/wireguard/wg0.conf

Jetzt kann das Wireguard-Interface gestartet werden:

wg-quick up wg0

  [#] ip link add wg0 type wireguard
  [#] wg setconf wg0 /dev/fd/63
  [#] ip -4 address add 10.10.100.5/24 dev wg0
  [#] ip link set mtu 1420 up dev wg0

Damit ist unser Server betriebsbereit.

Die Konfiguration für einen Raspi könnte so aussehen:

[Interface]
Address = 10.10.100.20/32
PrivateKey = <private_key_vom_raspi>

[Peer]
PublicKey = XzF1p6qb8zEBXWgI1b8Mqv4Z9qksTU/tTIEFAROMHEo=
Endpoint = 99.22.11.22:51820
# Route only vpn trafic through vpn
AllowedIPs = 10.10.100.0/24
# Route ALL traffic through vpn
#AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 21
wg set wg0 peer <public_key_vom_raspi> = allowed-ips 10.10.100.20/32
wg-quick save wg0  # auf der disk

Erweitertes VPN Setup #

Die erste Version der Netzwerktopologie war noch recht einfach gehalten: Eine überschaubare Anzahl von RaspberryPi Devices (ca. 20) verbinden sich mit einem zentralen Wireguard-VPN-Server und liefern dort Nutzdaten und Performancedaten ab; quasi ein sternförmiges Setup, mit dem Server in der Mitte. Nachdem sich die Anwendung als sehr beliebt herausgestellt hat und immer mehr Daten erhoben wurden wollte ich die REST-API für die Datenerhebung physikalisch vom VPN-Server und dem Analyse-Server trennen. Es kommen damit zwei peers hinzu, die von den Raspis erreicht werden sollen.

graph TD subgraph Raspis["Raspi-Flotte (~20 Peers)"] R1[Raspi] R2[Raspi] R3[…] end VPN[["VPN-Server
10.10.100.5"]] API["API-Server
10.10.100.30"] ANALYSE["Analyse-Server
10.10.100.31"] R1 -->|wg0| VPN R2 -->|wg0| VPN R3 -->|wg0| VPN VPN -->|FORWARD -d 10.10.100.30| API VPN -->|FORWARD -d 10.10.100.31| ANALYSE

API- und Analyse-Server laufen also nicht mehr auf dem VPN-Server selbst, sondern als eigene Hosts, die über den VPN-Server erreicht werden – der wird damit vom Endpunkt zum Router zwischen den Raspis und den beiden neuen Peers.

Eigentlich auch kein Problem: Die beiden neuen Server bekommen eine eigene VPN-Konfiguration und verbinden mit dem Wireguard-Server. Auf diesem Server muss jetzt noch die Konfiguration so angepasst werden, dass die eingehenden Pakete weitergeleitet werden können.

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; \
  iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; \
  ip6tables -A FORWARD -i wg0 -j ACCEPT; \
  ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; \
  iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; \
  ip6tables -D FORWARD -i wg0 -j ACCEPT; \
  ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Die Firewall #

Bis hierher lauscht der Server auf genau einem Port, 51820/udp, für den WireGuard-Handshake – und sonst reicht wer immer will an der Kiste vorbei. Das wollte ich nicht dem Zufall überlassen und habe ufw draufgesetzt, weil es die paar iptables-Regeln, die ich wirklich brauche, in eine lesbare Zeile pro Regel übersetzt:

apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 51820/udp
ufw allow OpenSSH
ufw enable

ufw status verbose zeigt danach brav Default: deny (incoming), allow (outgoing), deny (routed) – und genau bei diesem letzten Wort, routed, habe ich mich zu früh gefreut.

Der Haken #

Meine Annahme war: deny (routed) heißt, ein Raspi kommt an keinen anderen Peer heran, außer ich erlaube es explizit mit ufw route allow. Ein ping von einem Raspi zum nächsten widerlegt das sofort – die Weiterleitung funktioniert klaglos, obwohl ich dafür nie eine ufw-Regel gesetzt habe.

Der Grund steckt im PostUp von weiter oben. wg-quick hängt sein iptables -A FORWARD -i wg0 -j ACCEPT direkt an die laufende Kernel-Chain, an ufw und dessen Konfigurationsdateien vorbei. iptables -L FORWARD -n --line-numbers zeigt, wo die Regel landet:

Chain FORWARD (policy DROP)
num  target                       prot opt in  out source     destination
3    ufw-before-logging-forward   all  --  *   *   0.0.0.0/0  0.0.0.0/0
4    ufw-before-forward           all  --  *   *   0.0.0.0/0  0.0.0.0/0
5    ufw-after-forward            all  --  *   *   0.0.0.0/0  0.0.0.0/0
...
9    ACCEPT                       all  --  wg0 *   0.0.0.0/0  0.0.0.0/0

Meine eigene Regel läuft als letzte Zeile der Chain, nach allem, was ufw selbst verwaltet – und ein pauschales ACCEPT für alles von wg0 gewinnt immer, egal was ufw route allow/deny weiter oben dazu sagen würde. Ein ufw reload räumt das nicht auf, weil wg-quick diese Regel bei jedem wg-quick up wg0 ohnehin neu setzt, ufw-Zustand hin oder her.

Für einen VPN mit einer Handvoll vertrauenswürdiger Peers ist das egal. Für den Hub mit ca. 20 Raspis, von denen ich keinem hundertprozentig traue, ist es das nicht: AllowedIPs verhindert nur, dass ein Raspi die Absenderadresse eines anderen benutzt – wohin sein Paket danach darf, sagt das nicht. Ein kompromittierter Raspi käme also ungehindert bei allen anderen vorbei.

Gezielt statt pauschal #

Die Lösung liegt nicht in ufw, sondern darin, das pauschale ACCEPT im eigenen PostUp durch die tatsächlich gebrauchten Ziele zu ersetzen – jeder Raspi darf zum API- und zum Analyse-Server, aber zu keinem anderen Peer:

PostUp = iptables -A FORWARD -i wg0 -d 10.10.100.30 -j ACCEPT; \
  iptables -A FORWARD -i wg0 -d 10.10.100.31 -j ACCEPT; \
  iptables -A FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; \
  iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -d 10.10.100.30 -j ACCEPT; \
  iptables -D FORWARD -i wg0 -d 10.10.100.31 -j ACCEPT; \
  iptables -D FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; \
  iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

10.10.100.30 und .31 sind API- und Analyse-Server aus dem “Erweitertes VPN Setup” oben. Die dritte Zeile lässt nur noch die Antworten auf diese beiden Verbindungen zurück zum jeweiligen Raspi – ohne sie könnte der API-Server zwar antworten, aber nichts von sich aus zu einem Raspi öffnen, was für dieses Setup ohnehin nie gebraucht wird. Ein FORWARD von Raspi zu Raspi taucht in der Chain gar nicht mehr auf und landet damit in der Default-DROP-Policy.

Fazit #

ufw ist nach wie vor die richtige Wahl, um den Server selbst dichtzumachen – ein offener Port für den Handshake, der Rest zu. Für das Verhalten zwischen den Peers ist es aber die falsche Stelle: Das entscheidet WireGuards eigenes PostUp/PostDown, bevor ufw überhaupt zum Zug kommt. Wer eine Flotte von Geräten aufbaut, die sich gegenseitig nicht sehen sollen, muss dort ansetzen – nicht bei ufw route. 🔒