Send empty binary keepalive frames on signal WebSockets and ignore empty RustDesk heartbeat replies across the WS protobuf adapter. This prevents reverse-proxied WSS clients from timing out after the advertised keep_alive interval. Document the Nginx /ws/id and /ws/relay upstream mapping for BetterDesk Docker deployments. Fixes #144 Reported-by: @odixz
18 KiB
HTTPS Setup Guide
BetterDesk Console supports native HTTPS with TLS certificates, as well as reverse proxy configurations with Caddy or Nginx.
Quick Start
Option 1: Native HTTPS (Self-Signed Certificate)
Generate a self-signed certificate for testing:
# Linux
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /opt/rustdesk/ssl/privkey.pem \
-out /opt/rustdesk/ssl/fullchain.pem \
-subj "/CN=betterdesk.local"
# Windows (PowerShell)
$cert = New-SelfSignedCertificate -DnsName "betterdesk.local" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(1)
Export-PfxCertificate -Cert $cert -FilePath C:\RustDesk\ssl\cert.pfx -Password (ConvertTo-SecureString -String "password" -Force -AsPlainText)
# Convert to PEM with OpenSSL or use .pfx directly
Then edit your .env file:
HTTPS_ENABLED=true
HTTPS_PORT=5443
SSL_CERT_PATH=/opt/rustdesk/ssl/fullchain.pem
SSL_KEY_PATH=/opt/rustdesk/ssl/privkey.pem
HTTP_REDIRECT_HTTPS=true
Restart the console service and access it at https://your-server:5443.
Option 2: Let's Encrypt (Production)
Using Certbot:
# Install certbot
sudo apt install certbot
# Get certificate (standalone mode - stop BetterDesk console first)
sudo systemctl stop betterdesk-console
sudo certbot certonly --standalone -d console.yourdomain.com
sudo systemctl start betterdesk-console
Update .env:
HTTPS_ENABLED=true
HTTPS_PORT=443
SSL_CERT_PATH=/etc/letsencrypt/live/console.yourdomain.com/fullchain.pem
SSL_KEY_PATH=/etc/letsencrypt/live/console.yourdomain.com/privkey.pem
SSL_CA_PATH=/etc/letsencrypt/live/console.yourdomain.com/chain.pem
HTTP_REDIRECT_HTTPS=true
Set up auto-renewal:
# Add to crontab
0 0 1 * * certbot renew --pre-hook "systemctl stop betterdesk-console" --post-hook "systemctl start betterdesk-console"
Option 3: Reverse Proxy with Caddy (Recommended for Production)
Caddy automatically provisions and renews HTTPS certificates.
# Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
Create /etc/caddy/Caddyfile:
console.yourdomain.com {
reverse_proxy localhost:5000
# Optional: compress responses
encode gzip zstd
# Security headers (Caddy adds HSTS by default)
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
}
sudo systemctl enable caddy
sudo systemctl start caddy
With Caddy, leave HTTPS_ENABLED=false in .env since Caddy handles TLS termination.
Option 4: Reverse Proxy with Nginx
Install Nginx and Certbot:
sudo apt install nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/betterdesk:
# Upstream map for WebSocket connection upgrade
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# BetterDesk Console
server {
listen 80;
server_name console.yourdomain.com;
# Increase client body size for file uploads
client_max_body_size 100M;
# ─────────────────────────────────────────────────────────────────────────
# Console WebSocket endpoints (Web Remote Client, Chat, Relay)
# These require special handling for long-lived connections.
# RustDesk client WSS uses exact /ws/id and /ws/relay locations below.
# ─────────────────────────────────────────────────────────────────────────
location ~ ^/ws/ {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
# WebSocket upgrade headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Preserve client info
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable buffering for real-time streaming (JPEG frames)
proxy_buffering off;
proxy_cache off;
# Long timeouts for persistent WebSocket connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# Keepalive
proxy_socket_keepalive on;
}
# ─────────────────────────────────────────────────────────────────────────
# Standard HTTP requests (dashboard, API, static files)
# ─────────────────────────────────────────────────────────────────────────
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Fallback WebSocket support for non-/ws/ paths (legacy)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400s;
}
}
sudo ln -s /etc/nginx/sites-available/betterdesk /etc/nginx/sites-enabled/
sudo nginx -t # Validate configuration
sudo certbot --nginx -d console.yourdomain.com
sudo systemctl restart nginx
With Nginx reverse proxy, leave HTTPS_ENABLED=false in .env.
Important for Web Remote Client:
- The
proxy_buffering offdirective is critical for real-time JPEG streaming - Long timeouts (86400s) prevent WebSocket disconnections during idle periods
- The
mapdirective ensures proper WebSocket upgrade handling
RustDesk Client WSS Through Nginx
RustDesk's native client and web client use the Go server WebSocket ports, not the Node.js console WebSocket routes:
| Public path | Upstream | Purpose |
|---|---|---|
/ws/id |
21118 |
Rendezvous / ID server over WebSocket |
/ws/relay |
21119 |
Relay server over WebSocket |
If Nginx runs on the Docker host, proxy to the published localhost ports. If
Nginx runs in the same Docker network, replace 127.0.0.1 with the BetterDesk
server container name, for example betterdesk-server or betterdesk.
# RustDesk / BetterDesk signal WebSocket (hbbs-compatible)
location = /ws/id {
proxy_pass http://127.0.0.1:21118;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
# RustDesk / BetterDesk relay WebSocket (hbbr-compatible)
location = /ws/relay {
proxy_pass http://127.0.0.1:21119;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
Notes:
- Build or configure RustDesk clients with
allow-websocket=Ywhen you want the native client to use WSS instead of TCP/UDP signaling. - Do not point
/ws/idor/ws/relayat the console port (5000). These paths must reach the Go server ports21118and21119. - Keep these as exact
location = ...entries when your server block also has a generic consolelocation ~ ^/ws/rule. - Keep
proxy_read_timeoutabove 60 seconds. RustDesk expects long-lived signal WebSockets and uses empty binary frames as keepalive traffic. - When using only WSS on port 443, remove hard-coded relay values such as
host:21117from the client or console relay settings; otherwise the client may still try the raw TCP relay port.
Environment Variables Reference
| Variable | Default | Description |
|---|---|---|
HTTPS_ENABLED |
false |
Enable native HTTPS server |
HTTPS_PORT |
5443 |
HTTPS listening port |
SSL_CERT_PATH |
(empty) | Path to SSL certificate (PEM format) |
SSL_KEY_PATH |
(empty) | Path to SSL private key (PEM format) |
SSL_CA_PATH |
(empty) | Path to CA bundle / chain (optional) |
HTTP_REDIRECT_HTTPS |
true |
Redirect HTTP traffic to HTTPS when HTTPS is enabled |
Security Notes
When HTTPS is enabled, BetterDesk Console automatically:
- Enables HSTS (Strict-Transport-Security) header with 1 year max-age
- Sets
Secureflag on session cookies - Enables
upgrade-insecure-requestsCSP directive - Enables Cross-Origin-Opener-Policy
same-origin - Allows
wss://in Content-Security-Policy for future WebSocket connections
When HTTPS is not enabled (default), these stricter policies are disabled to avoid breaking HTTP-only deployments on internal networks.
Firewall Rules
If you enable native HTTPS, make sure to open the HTTPS port:
# Linux (ufw)
sudo ufw allow 5443/tcp
# Linux (firewalld)
sudo firewall-cmd --permanent --add-port=5443/tcp
sudo firewall-cmd --reload
# Windows
New-NetFirewallRule -DisplayName "BetterDesk HTTPS" -Direction Inbound -Protocol TCP -LocalPort 5443 -Action Allow
Troubleshooting
"HTTPS enabled but certificates not found/invalid"
The server will log this warning and fall back to HTTP mode. Check:
- Certificate file paths in
.envare correct - Files are readable by the BetterDesk process (check permissions)
- Certificate format is PEM (not DER or PFX)
Certificate Permission Errors
Let's Encrypt certificates are often readable only by root:
# Allow BetterDesk to read certificates
sudo chmod 644 /etc/letsencrypt/live/console.yourdomain.com/fullchain.pem
sudo chmod 640 /etc/letsencrypt/live/console.yourdomain.com/privkey.pem
sudo chgrp root /etc/letsencrypt/live/console.yourdomain.com/privkey.pem
Mixed Content Warnings
If you access the console via HTTPS but see mixed content warnings, ensure HTTPS_ENABLED=true is set so the security middleware enables upgrade-insecure-requests.
Behind a Reverse Proxy
When using a reverse proxy (Caddy/Nginx), keep HTTPS_ENABLED=false and let the proxy handle TLS. The proxy should set X-Forwarded-Proto: https so the application knows the original protocol. Express trusts proxy headers when configured—this is handled automatically.
Web Remote Client Not Working Through Nginx
If the web remote desktop client connects but shows "requesting connection" indefinitely:
-
Verify WebSocket upgrade is working:
# Test WebSocket endpoint curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \ https://console.yourdomain.com/ws/bd-signal # Should return "HTTP/1.1 101 Switching Protocols" -
Check nginx
proxy_bufferingis disabled for/ws/paths (see config above) -
Verify timeouts are long enough —
proxy_read_timeout 86400s -
Check nginx error logs:
sudo tail -f /var/log/nginx/error.log -
Ensure the desktop agent (BetterDesk Client) can reach the server. The agent must connect to
/ws/remote-agent/<device_id>before the browser viewer can stream.
BetterDesk Server (Go) WebSocket Ports
The BetterDesk Go server also exposes WebSocket endpoints for RustDesk protocol:
| Port | Protocol | Purpose |
|---|---|---|
| 21118 | WS/WSS | Signal WebSocket (RustDesk client signaling) |
| 21119 | WS/WSS | Relay WebSocket (RustDesk client data relay) |
These ports are used by the native RustDesk desktop client (not the web console). If you need to proxy them through nginx:
# Optional: Proxy RustDesk native client WebSocket (if needed)
server {
listen 21118;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:21118;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400s;
}
}
server {
listen 21119;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:21119;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400s;
}
}
Note: For most deployments, you do NOT need to proxy ports 21118/21119 — let clients connect directly to the Go server.
Installer SSL Configuration (Option C)
The BetterDesk ALL-IN-ONE installers (betterdesk.sh, betterdesk.ps1, betterdesk-docker.sh) include a built-in SSL configuration menu accessible via Option C in the main menu.
SSL Menu Options
| Option | Description |
|---|---|
| 1. Let's Encrypt | Automated certificate provisioning (requires port 80 and valid DNS) |
| 2. Custom Certificate | Use your own certificate from a CA or existing infrastructure |
| 3. Self-Signed Certificate | Generate a self-signed cert (development/testing/LAN only) |
| 4. Disable SSL | Remove TLS configuration, run in HTTP-only mode |
| 5. Enterprise TLS | Full HTTPS on ALL ports including Go server API (21114) |
During Fresh Install
After a successful fresh installation, the installer prompts:
🔒 Enterprise TLS enables full HTTPS on ALL ports (panel, signal, relay, API)
Recommended for production. Requires RustDesk client >= 1.3.x
Would you like to configure HTTPS Enterprise now? (Option 5 in SSL menu) [y/N]
Selecting "y" opens the SSL configuration menu where you can choose Option 5 for full Enterprise TLS.
Enterprise TLS (Full HTTPS on All Ports)
Enterprise TLS enables HTTPS/TLS on all BetterDesk ports, not just the web console:
| Port | Component | Without Enterprise TLS | With Enterprise TLS |
|---|---|---|---|
| 5000/5443 | Web Console | HTTP/HTTPS | HTTPS |
| 21114 | Go Server API | HTTP | HTTPS |
| 21116 | Signal Server (TCP) | Plain TCP | TLS |
| 21117 | Relay Server (TCP) | Plain TCP | TLS |
| 21118 | Signal WebSocket | WS | WSS |
| 21119 | Relay WebSocket | WS | WSS |
Requirements
- RustDesk client version 1.3.x or newer — older clients do not support TLS on signal/relay ports
- Valid TLS certificate (Let's Encrypt, custom CA, or self-signed for testing)
- Certificate SAN (Subject Alternative Name) should include:
- Domain name (e.g.,
betterdesk.example.com) - Public IP address
- LAN IP address (if used internally)
localhostand127.0.0.1(for local connections)
- Domain name (e.g.,
Go Server TLS Flags
The Go server supports the following TLS-related flags:
# Certificate paths
-tls-cert /path/to/fullchain.pem
-tls-key /path/to/privkey.pem
# Enable TLS per component
-tls-signal # TLS on signal port (21116)
-tls-relay # TLS on relay port (21117)
-tls-api # HTTPS on API port (21114)
# Force HTTPS redirect
-force-https # Implies -tls-api
Systemd Service Configuration (Linux)
When Enterprise TLS is enabled via the installer, the systemd service is configured with:
[Service]
ExecStart=/opt/rustdesk/betterdesk-server \
-key-dir /opt/rustdesk \
-db-path /opt/rustdesk/db_v2.sqlite3 \
-relay-servers YOUR_PUBLIC_IP:21117 \
-tls-cert /opt/rustdesk/ssl/betterdesk.crt \
-tls-key /opt/rustdesk/ssl/betterdesk.key \
-tls-signal \
-tls-relay
Environment="TLS_SIGNAL=Y"
Environment="TLS_RELAY=Y"
Node.js Console Configuration
The .env file is updated with:
HTTPS_ENABLED=true
HTTPS_PORT=5443
SSL_CERT_PATH=/opt/rustdesk/ssl/betterdesk.crt
SSL_KEY_PATH=/opt/rustdesk/ssl/betterdesk.key
HTTP_REDIRECT_HTTPS=true
ALLOW_SELF_SIGNED_CERTS=true # For self-signed certs (dev/LAN)
ENTERPRISE_TLS=true
Important Notes
-
Self-signed certificates and API: When using self-signed certificates, the Go server API (21114) is kept on HTTP to avoid breaking internal communication between Node.js console and Go server. Signal/relay ports still use TLS.
-
Browser certificate warnings: Self-signed certificates will cause browser warnings. Users must manually accept the certificate or add it to their trusted store.
-
RustDesk client configuration: Clients must be configured with the same server address. If using a domain with Let's Encrypt, ensure the domain resolves correctly.
-
Mixed TLS/plain connections: The Go server supports a "dual-mode listener" that auto-detects TLS vs plain connections on the same port (first-byte 0x16 detection). This allows gradual migration without breaking older clients.
Troubleshooting Enterprise TLS
Clients show "connection timeout" after enabling TLS
- Verify RustDesk client is version 1.3.x or newer
- Check that the Go server started successfully:
journalctl -u betterdesk-server -n 50 - Ensure certificate SAN includes the IP/domain the client is connecting to
"Failed to secure tcp: deadline has elapsed"
- The client is trying TLS but the server isn't configured for it (or vice versa)
- Check
-tls-signalflag is present in Go server ExecStart
Web console shows "0 devices" after enabling Enterprise TLS
- Internal Node.js → Go API communication may be broken if API is now HTTPS with self-signed
- For self-signed certs, check
ALLOW_SELF_SIGNED_CERTS=truein.env - Or keep API on HTTP (don't use
-tls-api) — only signal/relay need TLS for security