Zum Hauptinhalt springen
  1. Beiträge/
  2. Kleines Homelab/
  3. k3s-prod: Kubernetes Cluster Konfiguration und Anwendungsbereitstellung/

RustDesk-Server - Fernwartung im Tailnet

Inhaltsverzeichnis

RustDesk-Server
#

Selbst gehostete Gegenstelle für den RustDesk-Client: Geräte registrieren ihre ID beim ID-/Rendezvous-Server (hbbs), und wenn keine direkte Verbindung zustande kommt, läuft die Sitzung über das eigene Relay (hbbr). Damit verlässt kein Fernwartungsverkehr die eigene Infrastruktur.

Eingesetzt wird die quelloffene Variante. Web-Konsole, Benutzerverwaltung und Adressbuch gehören zur kostenpflichtigen Pro-Version und fehlen hier bewusst.

Erreichbarkeit
#

Der Server hat keine LoadBalancer-Adresse im LAN. Ein Tailscale-Sidecar meldet den Pod als eigenen Node rustdesk am Headscale an; Clients erreichen ihn unter rustdesk.hsn.zyria.de bzw. seiner Tailnet-Adresse.

1RustDesk-Client ──(Tailnet)──> Node "rustdesk"
2                                ├── hbbs   21115/tcp, 21116/tcp+udp, 21118/tcp
3                                └── hbbr   21117/tcp, 21119/tcp
4
5Browser ──(https)──> Traefik ──> Service ──> 21118 (/ws/id), 21119 (/ws/relay)

Container eines Pods teilen sich den Netzwerk-Namespace. tailscale0 liegt damit im selben Namespace wie die lauschenden RustDesk-Ports, eingehender Tailnet-Verkehr erreicht sie also ohne Portweiterleitung oder zusätzlichen Service.

TS_USERSPACE bleibt false: der Userspace-Modus reicht eingehendes UDP nicht zuverlässig an lokale Dienste weiter, und RustDesk registriert, hält seinen Heartbeat und versucht das NAT-Hole-Punching genau darüber (UDP 21116). Dafür braucht der Container /dev/net/tun und NET_ADMIN.

Aufbau
#

Ein Pod mit drei Containern und einem Init-Container:

Container Aufgabe
keygen (Init) erzeugt das Schlüsselpaar in /data, falls noch keines existiert
rustdesk hbbs und hbbr unter s6-Aufsicht
tailscale meldet den Pod als Node am Headscale an

Verwendet wird das s6-Image, nicht die klassischen Einzel-Images. Zwei Gründe:

  • hbbs und hbbr müssen sich dasselbe Schlüsselpaar in /data teilen; das klassische Deployment löst das über ein gemeinsames Volume für zwei Container.
  • Das klassische Image ist FROM scratch gebaut und enthält keine Shell. Der Start der beiden Prozesse ließe sich darin nicht aufeinander abstimmen.

Schlüsselpaar
#

Erzeugt wird es beim ersten Start vom Init-Container und liegt anschließend als id_ed25519 (privat) und id_ed25519.pub (öffentlich) im Longhorn-Volume.

Das übernimmt bewusst nicht der Server selbst: fehlt die Datei, legt sowohl hbbs als auch hbbr ein eigenes Paar an. Beim allerersten Start passiert das potenziell gleichzeitig und mit unterschiedlichem Ergebnis - der zuletzt schreibende Prozess gewinnt die Datei, beide hätten aber verschiedene Schlüssel im Speicher, und ein Client käme nur an einem der beiden Dienste vorbei. Der Init-Container läuft, bevor irgendein Dienst startet, und schließt dieses Fenster.

Den öffentlichen Schlüssel braucht jeder Client. Er steht im Log des Init-Containers und lässt sich jederzeit auslesen:

1kubectl -n rustdesk exec deploy/rustdesk -c rustdesk -- cat /data/id_ed25519.pub

ENCRYPTED_ONLY=1 startet beide Dienste mit -k _: sie nehmen nur Clients an, die genau diesen Schlüssel mitbringen. Ohne den Schalter dürfte jeder Fremde, der den Server erreicht, ihn als Relay benutzen.

Geht das Volume verloren, ändert sich der Schlüssel und alle Clients müssen neu konfiguriert werden. Ein Backup von id_ed25519 und id_ed25519.pub erspart das.

Webclient
#

Der Browser spricht wss, hbbs und hbbr sprechen einfaches ws. Traefik terminiert TLS auf rustdesk.zyria.de und reicht zwei Pfade an die WebSocket-Ports weiter:

Pfad Ziel
/ws/id 21118 (hbbs)
/ws/relay 21119 (hbbr)

Die Pfade sind vom RustDesk-Webclient fest vorgegeben und nicht frei wählbar.

Das Zertifikat kommt über den ClusterIssuer acmedns (DNS01), nicht über letsencrypt-production (HTTP01): der Host ist absichtlich nicht aus dem Internet erreichbar, eine HTTP01-Challenge könnte also nie beantwortet werden.

Damit der Name im LAN und im Tailnet auf Traefik zeigt, muss rustdesk.zyria.de auf dessen LoadBalancer-Adresse 192.168.178.23 bzw. 2003:a:b16:4700::1 aufgelöst werden.

Konfiguration
#

Vor dem ersten Deploy zu füllen:

  1. TS_AUTHKEY in kustomization.yaml - Pre-Auth-Key aus Headscale, mit tag:rustdesk als Forced Tag:
1kubectl -n headscale exec deploy/headscale -- \
2  headscale preauthkeys create --user <user> --tags tag:rustdesk --reusable --expiration 8760h

Der Key wird nur beim ersten Start verbraucht. Danach liegt der Node-State im Secret rustdesk-tailscale-state, das tailscaled selbst pflegt (siehe rbac.yaml). Dadurch behält der Node über Neustarts hinweg dieselbe Tailnet-Adresse - genau die, auf die die Clients konfiguriert sind.

Kein --advertise-tags in TS_EXTRA_ARGS: serverseitig vom Pre-Auth-Key gesetzt ist der Tag autoritativ und vom Client nicht überschreibbar. Schickt der Client den Tag zusätzlich selbst, löst das eine clientseitige Prüfung gegen tagOwners aus und die Registrierung schlägt fehl.

Zugriffskontrolle
#

apps/headscale/config-patch.yaml enthält dafür zwei Ergänzungen: tag:rustdesk unter tagOwners und eine Regel, die group:user den Zugriff auf die Ports des Servers erlaubt:

1{ "action": "accept", "src": ["group:user"], "dst": ["tag:rustdesk:21115-21119"] }

Bewusst ein eigener Tag statt tag:router: der Node annonciert keine Routen, und die breite tag:router-Regel soll nicht davon abhängen, welche Dienste zufällig denselben Tag tragen. Der Portbereich deckt hbbs (21115, 21116 TCP und UDP, 21118) und hbbr (21117, 21119) ab.

Da die Policy keine Catch-all-Regel hat, gilt Default-Deny: wer nicht in group:user steht, erreicht den Server nicht.

Client-Einstellungen
#

Unter Einstellungen → Netzwerk → ID-/Relay-Server:

Feld Wert
ID-Server rustdesk.hsn.zyria.de
Relay-Server rustdesk.hsn.zyria.de:21117
Key Inhalt von id_ed25519.pub

Der Relay-Wert steht zusätzlich serverseitig in RELAY und wird von hbbs an die Clients weitergereicht. Er zeigt bewusst auf den MagicDNS-Namen und nicht auf 127.0.0.1: der Wert ist die Adresse, die die Clients ansprechen sollen, nicht eine lokale.

Da der Server nur im Tailnet liegt, muss auf jedem Client Tailscale laufen und angemeldet sein - andernfalls ist schon der ID-Server nicht auflösbar.

Betrieb
#

replicas: 1 mit strategy: Recreate. Der Pod hält eindeutigen State: die sqlite-Datenbank auf einem ReadWriteOnce-Volume und den Headscale-Node. Zwei gleichzeitig laufende Pods würden sich beides streitig machen.

Die Liveness-Probe hängt an Port 21116 (hbbs), die Readiness an 21117 (hbbr): ohne Relay ist der Dienst für alle Clients unbrauchbar, die keine direkte Verbindung zustande bringen.

1kubectl -n rustdesk logs deploy/rustdesk -c rustdesk
2kubectl -n rustdesk exec deploy/rustdesk -c tailscale -- tailscale status

Installation
#

Die Anwendung wird mittels Kustomize und Helm durch ArgoCD bereitgestellt; der Eintrag steht in manifests.yaml.

1kubectl kustomize --enable-helm apps/rustdesk | kubectl apply -f -

Abhängigkeiten
#

  • Laufender Headscale (apps/headscale) samt freigegebenem tag:rustdesk.
  • Traefik und cert-manager für den Webclient-Zugang.
  • Longhorn für das Volume mit Schlüsselpaar und Datenbank.
  • /dev/net/tun auf dem Node für den Kernel-Modus von tailscaled.

Offene Punkte
#

Der Headscale-Pre-Auth-Key liegt im Klartext im Repository - konsistent mit dem restlichen Repo, aber weiterhin die offene Security-Aufgabe (SOPS oder Sealed Secrets).

Die quelloffene Variante kennt keine Benutzerverwaltung: wer den öffentlichen Schlüssel und eine RustDesk-ID hat, kann eine Sitzung aufbauen. Die Zugriffskontrolle stützt sich hier vollständig auf die Headscale-ACL und auf das Passwort, das der jeweilige Client vergibt.

Fabrice Kirchner
Autor
Fabrice Kirchner
stolzer Vater, Nerd, Admin