Héberger RustDesk dans Kubernetes avec client Web

Vous souhaitez auto-héberger RustDesk (alternative libre à AnyDesk/TeamViewer) dans un cluster Kubernetes avec interface Web, WebSocket WSS et intégration complète ? Voici une solution prête à l’emploi avec MetalLB, Let’s Encrypt, liveness/readiness probes, et un déploiement clef en main du web client.


Prérequis

  • Un cluster Kubernetes fonctionnel
  • MetalLB configuré (pour attribuer une IP publique au service LoadBalancer)
  • Cert-manager installé avec un issuer letsencrypt-prod
  • Un nom de domaine configuré vers l’IP fournie par MetalLB (ex. rustdesk.test.local)

Comment récupérer la clé (KEY) ?

Lors du premier démarrage de votre serveur RustDesk, une paire de clés est générée automatiquement. Vous pouvez extraire la clé publique (à injecter côté client) via la commande suivante :

kubectl exec -n rustdesk deploy/rustdesk-server -c hbbs -- cat /root/id_ed25519.pub

Cette clé est à renseigner dans la variable d’environnement KEY du web client pour établir des connexions sécurisées.


Client Web accessible

Vous pouvez tester l’interface Web ici : https://rustdesk-web-client.pascal-mietlicki.fr


Adresse IP externe à utiliser dans le client lourd

Pour connaître l’IP publique exposée par MetalLB sur laquelle pointer les ports 21116 (registry/heartbeat) et 21117 (relay), utilisez :

kubectl get svc -n rustdesk rustdesk-server

Exemple de sortie :

NAME             TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)
rustdesk-server  LoadBalancer   10.43.120.151   192.168.100.5  21115:... 21116:... 21117:...

Dans le client RustDesk natif, configurez :

  • ID Server : 192.168.100.5:21116
  • Relay Server : 192.168.100.5:21117

Vous aurez aussi besoin de la clef que vous avez récupéré dans l’étape précédente, elle sera à utiliser à la fois dans le client lourd mais aussi dans le client Web (env KEY).


Namespace RustDesk

apiVersion: v1
kind: Namespace
metadata:
  name: rustdesk

Volume persistant pour les clés et les données

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rustdesk-data
  namespace: rustdesk
  labels:
    app: rustdesk-server
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 5Gi

Déploiement du serveur RustDesk (hbbs + hbbr)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rustdesk-server
  namespace: rustdesk
  labels:
    app: rustdesk-server
spec:
  replicas: 1
  selector:
    matchLabels: { app: rustdesk-server }
  template:
    metadata:
      labels: { app: rustdesk-server }
    spec:
      containers:
        - name: hbbs
          image: docker.io/rustdesk/rustdesk-server:latest
          imagePullPolicy: IfNotPresent
          command: ["hbbs"]
          args: ["-k","_"]
          ports:
            - name: nat-port
              containerPort: 21115
              protocol: TCP
            - name: registry-port
              containerPort: 21116
              protocol: TCP
            - name: heartbeat-port
              containerPort: 21116
              protocol: UDP
            - name: web-port
              containerPort: 21118
              protocol: TCP
          livenessProbe:
            tcpSocket: { port: 21115 }
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            tcpSocket: { port: 21115 }
            initialDelaySeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: rustdesk-data
              mountPath: /root
        - name: hbbr
          image: docker.io/rustdesk/rustdesk-server:latest
          imagePullPolicy: IfNotPresent
          command: ["hbbr"]
          args: ["-k","_"]
          ports:
            - name: relay-port
              containerPort: 21117
              protocol: TCP
            - name: client-port
              containerPort: 21119
              protocol: TCP
          livenessProbe:
            tcpSocket: { port: 21117 }
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            tcpSocket: { port: 21117 }
            initialDelaySeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: rustdesk-data
              mountPath: /root
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels: { app: rustdesk-server }
              topologyKey: kubernetes.io/hostname
      volumes:
        - name: rustdesk-data
          persistentVolumeClaim:
            claimName: rustdesk-data

Service LoadBalancer exposé via MetalLB

apiVersion: v1
kind: Service
metadata:
  name: rustdesk-server
  namespace: rustdesk
  labels:
    app: rustdesk-server
spec:
  type: LoadBalancer
  externalTrafficPolicy: Cluster
  selector: { app: rustdesk-server }
  ports:
    - name: nat-port
      port: 21115
      targetPort: 21115
      protocol: TCP
    - name: registry-port
      port: 21116
      targetPort: 21116
      protocol: TCP
    - name: heartbeat-port
      port: 21116
      targetPort: 21116
      protocol: UDP
    - name: web-port
      port: 21118
      targetPort: 21118
      protocol: TCP
    - name: relay-port
      port: 21117
      targetPort: 21117
      protocol: TCP
    - name: client-port
      port: 21119
      targetPort: 21119
      protocol: TCP

Déploiement du Web Client

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rustdesk-web-client
  namespace: rustdesk
  labels:
    app: rustdesk-web-client
spec:
  replicas: 1
  selector:
    matchLabels: { app: rustdesk-web-client }
  template:
    metadata:
      labels: { app: rustdesk-web-client }
    spec:
      containers:
        - name: web-client
          image: pmietlicki/rustdesk-web-client:v1
          imagePullPolicy: Always
          ports:
            - containerPort: 5000
          env:
            - name: CUSTOM_RENDEZVOUS_SERVER
              value: "rustdesk.test.local"
            - name: RELAY_SERVER
              value: "rustdesk.test.local"
            - name: KEY
              value: "xxxxxxxxxxxxxxxxxxxxxxx"
          livenessProbe:
            httpGet: { path: "/", port: 5000 }
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet: { path: "/", port: 5000 }
            initialDelaySeconds: 5
            periodSeconds: 10

Service interne Web Client

apiVersion: v1
kind: Service
metadata:
  name: rustdesk-web-client
  namespace: rustdesk
  labels:
    app: rustdesk-web-client
spec:
  type: ClusterIP
  selector: { app: rustdesk-web-client }
  ports:
    - port: 5000
      targetPort: 5000
      protocol: TCP

Ingress unique avec WebSocket + HTTPS

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rustdesk
  namespace: rustdesk
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
spec:
  tls:
    - hosts: [rustdesk.test.local]
      secretName: rustdesk-server-tls
  rules:
    - host: rustdesk.test.local
      http:
        paths:
          - path: /ws/id
            pathType: Prefix
            backend:
              service: { name: rustdesk-server, port: { name: web-port } }
          - path: /ws/relay
            pathType: Prefix
            backend:
              service: { name: rustdesk-server, port: { name: client-port } }
          - path: /
            pathType: Prefix
            backend:
              service: { name: rustdesk-web-client, port: { number: 5000 } }

Pourquoi cette solution ?

  • Elle repose uniquement sur les images officielles RustDesk et une version web-client personnalisée disponible ici : docker-rustdesk-web-client.
  • Le système est sécurisé par TLS, supporte WSS, et peut être intégré dans un environnement Zero Trust.
  • Le tout est optimisé pour Kubernetes avec un déploiement industrialisé et facilement scalable.

✅ Compatible avec un usage interne ou externe.
🔐 Pas besoin de se connecter aux serveurs publics.
📡 Possibilité d’utiliser le web client dans un navigateur avec relais WebSocket sécurisé.