RustDesk Server
RustDesk Server OSS provides the rendezvous and relay infrastructure for self-hosted RustDesk remote desktop clients. This chart runs one coordinated hbbs/hbbr pair with persistent identity, restricted container privileges and real native protocol acceptance tests.
Requirements
- Kubernetes 1.29 or newer with native SidecarContainers enabled; Helm 3.
- Storage supporting the configured UID/GID and volume ownership (1000 by default).
- A reachable TCP/UDP endpoint, DNS/firewall rules and compatible RustDesk clients for access outside the cluster. The default Service is private ClusterIP.
- External Secrets Operator or an HTTP/Gateway controller only if their optional integrations are enabled.
The official image is docker.io/rustdesk/rustdesk-server:1.1.16, verified for
amd64, arm64 and arm/v7. RustDesk Server is AGPL-3.0; this chart is Apache-2.0.
There is no Pro console, external SQL database, multi-replica mode or Prometheus
endpoint in this OSS chart.
Install and connect
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install rustdesk helmforge/rustdesk-server \
--namespace rustdesk --create-namespace -f production-values.yaml
kubectl -n rustdesk logs deployment/rustdesk-rustdesk-server -c hbbs
Start production-values.yaml from an example, then
replace its hostnames, Secret reference and CIDRs. For a private initial trial,
omit -f production-values.yaml. Read the printed Helm NOTES for current resource
names and listener ports. This image contains no shell or file utilities.
Configure each client’s ID server, relay server and the public Key: from hbbs
logs. Never distribute the private key. server.relayServers accepts reachable
host:port entries that hbbs advertises. Empty means clients must configure their
relay endpoint explicitly. A matching public key is not per-user authorization;
configure endpoint access controls and network admission separately.
Ports and exposure
| Listener | Default | Purpose |
|---|---|---|
| TCP | 21115 | NAT test; rendezvous base minus one |
| TCP and UDP | 21116 | Native rendezvous |
| TCP | 21117 | Native relay |
| TCP, optional Service mapping | 21118 | Rendezvous WebSocket |
| TCP, optional Service mapping | 21119 | Relay WebSocket |
LoadBalancer requires provider support for mixed TCP/UDP. NodePort mappings must
match the public endpoint clients use. externalTrafficPolicy: Local preserves
source addresses at the Service boundary where possible; external SNAT can still
impair hole punching. The default NetworkPolicy permits same-namespace Pods only.
Set explicit ingressFrom peers for offices, VPNs and HTTP controllers.
See the networking guide for external mapping and client setup.
websocket.enabled adds the two WebSocket Service ports. Optional Ingress uses
distinct rendezvous and relay hostnames. gatewayAPI.httpRoutes[] selects hbbs
or hbbr through component and defaults to that component’s WebSocket backend.
These HTTP routes do not expose native TCP/UDP or install a web client. Configure
TLS and long-lived connection support in the chosen controller.
Identity, storage and availability
hbbs starts first as a native restartable init container and passes its startup
probe before hbbr launches. This prevents competing first-start key generation.
Both binaries explicitly require the same native server key using -k _.
Generated identity and SQLite peer associations persist together in a 1Gi RWO
PVC. Recreate updates deliberately interrupt sessions to avoid concurrent writers.
Exactly one pair is supported, including when the storage supports RWX.
Use auth.existingSecret for an existing matching native keypair. The Secret must
contain native base64 private/public file contents, not PEM, under the configurable
key names. The chart mounts both files read-only and keeps their contents out of
Helm values. See identity and security.
External Secrets Operator integration uses externalSecrets.enabled,
refreshInterval and items[] with complete per-item specs. Install the operator
and store independently, and point auth.existingSecret to the synchronized target.
An ESO example is included. Secret changes require
Pod replacement and identity rotation requires updating clients.
The chart retains its generated PVC on uninstall by default. Existing claims are
never created or deleted by the chart. Namespace deletion can still remove data.
Use persistence.existingClaim to reuse a retained or restored claim. Disabling
persistence is only for disposable trials. Back up the complete stopped data
directory and any separately managed identity source; a retained PVC is not a
backup. See backup, restore and upgrades.
Resources and operations
Each binary requests 100m CPU and 64Mi memory and has a 1 CPU/256Mi ceiling.
Size relay resources for concurrent sessions and measured network throughput.
hbbr.totalBandwidth, singleBandwidth and limitSpeed map to upstream limits
using units of 1024*1024 bits per second. Defaults are 1024, 128 and 32.
server.alwaysUseRelay forces relay use but still needs a reachable relay address.
Both containers run non-root with dropped capabilities, no privilege escalation, read-only image filesystems and RuntimeDefault seccomp. No Kubernetes API token is mounted. Standard scheduling, annotations, ServiceAccount and security context overrides are available; storage permissions must match the selected identity.
TCP probes check listener health. The mandatory rendezvous startup probe also enforces identity initialization order. Listener readiness does not prove internet NAT, client permission or desktop consent; verify those with real endpoints.
Validation
The chart includes template contract tests and CI profiles for defaults, custom ports/forced relay, dual-stack Services, disposable storage, existing identity, ESO and WebSocket routes. Its runtime client checks UDP registration and heartbeat, TCP NAT responses, wrong-key rejection and 64KiB payloads in both relay directions. Persistent profiles repeat checks after Pod replacement and quiesced recovery to a fresh PVC. WebSocket profiles exercise key enforcement and binary relay traffic.
make validate-chart CHART=rustdesk-server CONTEXT=k3d-helmforge-tests-wsl
Run that command from the HelmForge ops repository. The full gate includes Helm lint, all rendered CI profiles, helm-unittest, real CRD schemas, Artifact Hub lint and behavioral k3d validation. Cloud load balancer and external TLS acceptance remain deployment-specific. See design decisions.
Security Scan: rustdesk-server
| Framework | Score |
|---|---|
| MITRE + NSA + SOC2 | 100% |
Security posture acceptable.
Measured locally on 2026-09-14 with Kubescape 4.0.14 against the default rendered chart: 33 controls passed, zero failed. This is a Kubernetes configuration scan, not an application vulnerability audit or proof of internet NAT behavior.
Values reference
HTTPRoute and ExternalSecret list entries must have unique rendered names, including after the Kubernetes name-length limit is applied. Give multiple entries distinct names; duplicate effective names are rejected during rendering.
| Parameter | Default | Description |
|---|---|---|
nameOverride |
"" |
Override the chart name in resource names. |
fullnameOverride |
"" |
Override the complete resource name. |
replicaCount |
1 |
Exactly one coordinated rendezvous/relay pair; horizontal scaling is unsupported. |
commonLabels |
{} |
Additional resource labels; selector labels cannot be overridden. |
podLabels |
{} |
Additional Pod labels; selector labels cannot be overridden. |
podAnnotations |
{} |
Additional Pod annotations. |
imagePullSecrets |
[] |
Registry credentials for the official image. |
image |
{} |
Official upstream image shared by hbbs and hbbr. |
image.repository |
"docker.io/rustdesk/rustdesk-server" |
Official RustDesk Server OSS repository. |
image.tag |
"1.1.16" |
Pinned stable upstream version, independent of the chart version. |
image.pullPolicy |
"IfNotPresent" |
Image pull policy. |
server |
{} |
Native rendezvous and relay configuration. |
server.relayServers |
[] |
Client-reachable relay endpoints, including port, for example relay.example.com:21117. Empty requires clients to configure their relay explicitly. |
server.alwaysUseRelay |
false |
Force relay instead of attempting peer-to-peer hole punching. |
server.rendezvousPort |
21116 |
Rendezvous TCP/UDP base port; NAT is base-1 and WebSocket is base+2. |
server.relayPort |
21117 |
Relay TCP base port; relay WebSocket is base+2. |
auth |
{} |
Native server identity supplied independently of Helm release history. |
auth.existingSecret |
"" |
Secret containing a matching native base64 Ed25519 private/public pair. Empty generates the pair on the data volume. |
auth.privateKeyKey |
"id_ed25519" |
Secret key containing the native base64 64-byte private key. |
auth.publicKeyKey |
"id_ed25519.pub" |
Secret key containing the native base64 32-byte public key. |
externalSecrets |
{} |
Optional External Secrets Operator integration; install the operator separately. |
externalSecrets.enabled |
false |
Render ExternalSecret resources. |
externalSecrets.refreshInterval |
"1h" |
Default synchronization interval. |
externalSecrets.items |
[] |
Complete ExternalSecret definitions with optional name/fullnameOverride/labels/annotations and spec. |
websocket |
{} |
Optional WebSocket listeners for compatible clients; no web console is included. |
websocket.enabled |
false |
Expose the derived hbbs and hbbr WebSocket ports through the Service and NetworkPolicy. |
ingress |
{} |
HTTP Ingress for WebSocket endpoints only. |
ingress.enabled |
false |
Render one Ingress with separate rendezvous and relay hostnames. |
ingress.ingressClassName |
"" |
Ingress controller class; empty omits ingressClassName. |
ingress.annotations |
{} |
Controller annotations, including controller-specific WebSocket timeouts. |
ingress.rendezvousHost |
"" |
Hostname for the hbbs WebSocket endpoint. |
ingress.relayHost |
"" |
Distinct hostname for the hbbr WebSocket endpoint. |
ingress.tls |
[] |
Standard Ingress TLS entries. |
gatewayAPI |
{} |
Gateway API HTTPRoutes for WebSocket endpoints only. |
gatewayAPI.enabled |
false |
Render HTTPRoutes; requires websocket.enabled and an existing Gateway controller. |
gatewayAPI.httpRoutes |
[] |
Route definitions; component is hbbs (default) or hbbr. Supports name, parentRefs, hostnames, rules, labels and annotations. |
hbbs |
{} |
Rendezvous process settings. Starts before hbbr as a native sidecar. |
hbbs.resources |
{} |
Resource requests and limits for rendezvous and its SQLite database. |
hbbs.resources.requests |
{} |
Guaranteed scheduling resources. |
hbbs.resources.requests.cpu |
"100m" |
Requested CPU. |
hbbs.resources.requests.memory |
"64Mi" |
Requested memory. |
hbbs.resources.limits |
{} |
Resource ceilings. |
hbbs.resources.limits.cpu |
"1" |
CPU limit. |
hbbs.resources.limits.memory |
"256Mi" |
Memory limit. |
hbbr |
{} |
Relay process settings; bandwidth and session count drive sizing. |
hbbr.totalBandwidth |
1024 |
Aggregate relay bandwidth limit in upstream units of 1024*1024 bits per second. |
hbbr.singleBandwidth |
128 |
Per-session relay bandwidth limit in upstream Mb/s units. |
hbbr.limitSpeed |
32 |
Throttled/blacklisted session bandwidth in upstream Mb/s units. |
hbbr.resources |
{} |
Resource requests and limits for relay traffic. |
hbbr.resources.requests |
{} |
Guaranteed scheduling resources. |
hbbr.resources.requests.cpu |
"100m" |
Requested CPU. |
hbbr.resources.requests.memory |
"64Mi" |
Requested memory. |
hbbr.resources.limits |
{} |
Resource ceilings. |
hbbr.resources.limits.cpu |
"1" |
CPU limit. |
hbbr.resources.limits.memory |
"256Mi" |
Memory limit. |
serviceAccount |
{} |
Dedicated account; RustDesk does not call the Kubernetes API. |
serviceAccount.create |
true |
Create a ServiceAccount. |
serviceAccount.name |
"" |
Existing account name or custom created name. |
serviceAccount.annotations |
{} |
Account annotations. |
serviceAccount.automountServiceAccountToken |
false |
Mount a Kubernetes API token; unnecessary for both servers. |
podSecurityContext |
{} |
Pod identity and volume ownership. |
podSecurityContext.runAsNonRoot |
true |
Require non-root execution. |
podSecurityContext.runAsUser |
1000 |
Numeric runtime user. |
podSecurityContext.runAsGroup |
1000 |
Numeric primary group. |
podSecurityContext.fsGroup |
1000 |
Group ownership for the shared data volume. |
podSecurityContext.fsGroupChangePolicy |
"OnRootMismatch" |
Avoid recursive ownership walks when the volume root already matches. |
podSecurityContext.seccompProfile |
{} |
Kernel syscall filtering. |
podSecurityContext.seccompProfile.type |
"RuntimeDefault" |
Use the runtime default profile. |
securityContext |
{} |
Applied to both native server containers. |
securityContext.allowPrivilegeEscalation |
false |
Prevent privilege escalation. |
securityContext.readOnlyRootFilesystem |
true |
Application writes only into its data volume. |
securityContext.capabilities |
{} |
Linux capability configuration. |
securityContext.capabilities.drop |
["ALL"] |
Drop all capabilities; native ports are unprivileged. |
service |
{} |
Native TCP/UDP Service. Internet access requires an appropriate external endpoint. |
service.type |
"ClusterIP" |
ClusterIP, NodePort or LoadBalancer. |
service.annotations |
{} |
Provider-specific annotations. |
service.externalTrafficPolicy |
"Local" |
Preserve source IP for external Services; provider SNAT remains outside this setting. |
service.ipFamilyPolicy |
"" |
Optional IP policy: SingleStack, PreferDualStack or RequireDualStack. |
service.ipFamilies |
[] |
Optional ordered IP families, for example [IPv4, IPv6]. |
service.loadBalancerClass |
"" |
Optional LoadBalancer implementation class, immutable once allocated. |
service.loadBalancerSourceRanges |
[] |
Optional CIDRs accepted by a compatible LoadBalancer implementation. |
service.nodePorts |
{} |
Explicit external node ports; zero delegates allocation to Kubernetes. |
service.nodePorts.nat |
0 |
NAT test TCP port. |
service.nodePorts.rendezvous |
0 |
Rendezvous TCP port. |
service.nodePorts.rendezvousUdp |
0 |
Rendezvous UDP port; normally use the same public number as TCP. |
service.nodePorts.relay |
0 |
Relay TCP port. |
service.nodePorts.rendezvousWs |
0 |
Rendezvous WebSocket TCP port when enabled. |
service.nodePorts.relayWs |
0 |
Relay WebSocket TCP port when enabled. |
persistence |
{} |
Retain SQLite peer identities and the native Ed25519 keypair together. |
persistence.enabled |
true |
Use persistent storage. Disable only for disposable trials with disposable keys. |
persistence.existingClaim |
"" |
Existing PVC; never created or deleted by this chart. |
persistence.storageClass |
"" |
StorageClass; empty uses the cluster default, ‘-’ disables dynamic provisioning. |
persistence.size |
"1Gi" |
Requested data-volume capacity. |
persistence.accessModes |
["ReadWriteOnce"] |
Volume access modes; RWX does not permit concurrent server replicas. |
persistence.retain |
true |
Retain a generated PVC after Helm uninstall; namespace deletion is not protected. |
persistence.annotations |
{} |
Additional PVC annotations. |
networkPolicy |
{} |
Protect native listener ports with explicit client peers. |
networkPolicy.enabled |
true |
Render a NetworkPolicy; enforcement requires a compatible CNI. |
networkPolicy.ingressFrom |
[] |
Allowed native clients. Empty restricts to Pods in the release namespace. |
networkPolicy.egressIsolation |
true |
Restrict outbound connections to DNS and explicitly configured rules. |
networkPolicy.dnsEgress |
[{"namespaceSelector":{}}] |
DNS destinations on TCP/UDP 53. |
networkPolicy.extraEgress |
[] |
Additional complete egress rules. |
probes |
{} |
TCP probe timing. The mandatory hbbs startup probe serializes key initialization. |
probes.startup |
{} |
Startup probe budget. |
probes.startup.periodSeconds |
5 |
Interval between attempts. |
probes.startup.timeoutSeconds |
2 |
Per-attempt timeout. |
probes.startup.failureThreshold |
60 |
Consecutive failures before restart. |
probes.readiness |
{} |
Readiness probe timing for both components. |
probes.readiness.periodSeconds |
5 |
Interval between attempts. |
probes.readiness.timeoutSeconds |
2 |
Per-attempt timeout. |
probes.readiness.failureThreshold |
3 |
Failures before marking unready. |
probes.liveness |
{} |
Liveness probes detect a stopped native listener. |
probes.liveness.periodSeconds |
20 |
Interval between attempts. |
probes.liveness.timeoutSeconds |
2 |
Per-attempt timeout. |
probes.liveness.failureThreshold |
3 |
Failures before restart. |
terminationGracePeriodSeconds |
30 |
Shutdown grace period; active remote desktop sessions are interrupted on replacement. |
nodeSelector |
{} |
Node selection labels. |
tolerations |
[] |
Pod tolerations. |
affinity |
{} |
Pod affinity and anti-affinity. |
priorityClassName |
"" |
Optional workload priority. |
topologySpreadConstraints |
[] |
Optional topology constraints; this does not increase replicas. |