Summary
When hbbs (RustDesk rendezvous server) starts inside a Docker container with a bind-mounted data directory (./data:/root), it inadvertently creates a ~/.config/rustdesk/RustDesk.toml file inside the data directory. This file is the same client-side config format used by RustDesk desktop/mobile clients and should not exist in the server's working directory.
The file is created by hbb_common (a shared library used by both client and server), specifically through Config::get_key_pair() being called at startup via a fallback path in get_uuid().
Affected Component
- Binary:
hbbs (rendezvous server)
- Not affected:
hbbr (relay server) — does not use hbb_common::Config startup chain that triggers config write
- Library:
libs/hbb_common in both rustdesk-server and rustdesk repos
Environment
- Deployment: Docker with
volumes: ./data:/root (bind mount)
- Host path affected:
/root/docker/rustdesk/data/.config/rustdesk/RustDesk.toml
- Permissions:
0o600 (owned by root)
- Container user: root
Root Cause Analysis
Trigger chain
The entry point is check_software_update() in the hbbs main thread, which spawns a worker thread. That worker calls version_check_request(), which invokes get_fingerprint() — and FingerprintingInfo::new() accesses Config::get_id(), which triggers the CONFIG lazy_static initializer (and thus Config::load()).
hbbs main() [src/main.rs:10]
├─ init_args() [src/common.rs:56]
├─ check_software_update() [src/common.rs:193]
│ └─ thread: check_software_update_() [src/common.rs:201]
│ └─ version_check_request(...) [hbb_common/src/lib.rs:425]
│ └─ fingerprint::get_fingerprint(None, None) [hbb_common/src/lib.rs:434]
│ └─ FingerprintingInfo::new() [hbb_common/src/fingerprint.rs:198]
│ └─ Config::get_id() [hbb_common/src/fingerprint.rs:203]
│ └─ CONFIG.read() TRIGGERS lazy_static!
│ └─ Config::load() [hbb_common/src/config.rs:53]
│ ├─ decrypt_str_or_original(password)
│ ├─ decrypt_str_or_original(enc_id)
│ ├─ get_auto_id() [MAC succeeds in Docker]
│ │ └─ config.id = "auto_id_str"
│ │ └─ store = true
│ └─ config.store() [hbb_common/src/config.rs:573]
│ ├─ encrypt_str_or_original(config.id)
│ │ └─ encrypt()
│ │ └─ symmetric_crypt()
│ │ └─ get_uuid()
│ │ └─ Config::get_key_pair().1
│ │ ├─ Config::load_<Config>("") [line 893]
│ │ ├─ key_pair empty → gen_keypair()
│ │ ├─ spawn background thread:
│ │ │ CONFIG.write() → set key_pair → store()
│ │ └─ return pk
│ └─ Config::store_(&config, "") WRITE RustDesk.toml
│
└─ RendezvousServer::start(...) [src/main.rs:35]
└─ get_server_sk(key) [src/rendezvous_server.rs:1229]
└─ crate::common::gen_sk(0) [src/common.rs:107]
└─ reads id_ed25519 (relative path)
Why key_pair is written separately
Config::get_key_pair() operates independently of gen_sk(). It:
- Locks
KEY_PAIR cache — misses
- Loads
~/.config/rustdesk/RustDesk.toml via Config::load_("")
- Finds
key_pair.0 empty → generates sign::gen_keypair()
- Spawns a background thread that acquires
CONFIG.write() and calls config.store() again with the populated keypair
Result: two keypair storage mechanisms coexist independently:
| Mechanism |
Path |
Trigger |
Write time |
gen_sk() (server-native) |
./id_ed25519 |
RendezvousServer::start() |
+1.1ms |
Config::get_key_pair() (shared lib) |
~/.config/rustdesk/RustDesk.toml |
check_software_update() → T1 |
+0ms |
Why the path lands in the data directory
On Linux, directories_next::ProjectDirs::from("", "", "RustDesk") resolves to:
/root/.config/rustdesk/RustDesk.toml
Because Docker is started as root with volumes: ./data:/root, the host path ./data/.config/rustdesk/RustDesk.toml is created.
The patch() function at config.rs:400 only redirects /root to /home/<user> when the effective user is not root — but inside the container the EUID is 0, so the redirect never triggers.
Why get_uuid() calls get_key_pair() on the server
get_uuid() at lib.rs:300 is a cross-platform machine identifier used by password security and fingerprinting:
pub fn get_uuid() -> Vec<u8> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(id) = machine_uid::get() {
return id.into();
}
Config::get_key_pair().1 // fallback — client code, not server code
}
On Linux containers, machine_uid::get() reads /etc/machine-id or /var/lib/dbus/machine-id, which may not exist or may be inaccessible. When it fails, the fallback Config::get_key_pair().1 executes — invoking the client-side config system unnecessarily in a server context.
Notice the timestamp ordering from container logs confirms the call sequence:
[2026-05-13 07:55:40.524858 +00:00] INFO [libs/hbb_common/src/config.rs:895] Generated new keypair for id:
[2026-05-13 07:55:40.525990 +00:00] INFO [src/common.rs:121] Private key comes from id_ed25519
[2026-05-13 07:55:40.525997 +00:00] INFO [src/rendezvous_server.rs:1251] Key: SR5mXVPV7z5NzH3lFeOio0TRzJve4YF9iQTdR4fsgAA=
[2026-05-13 07:55:40.526001 +00:00] INFO [src/peer.rs:84] DB_URL=./db_v2.sqlite3
get_key_pair() (via check_software_update thread) fires first, followed by gen_sk() (via RendezvousServer::start). The ~1ms gap matches thread scheduling.
File Contents
data/.config/rustdesk/RustDesk.toml contains the encrypted ID and keypair:
enc_id = '00ZR49a5Jx88J4wzF7ZUQq6a1AFuW3EV7psg=='
password = ''
salt = ''
key_pair = [[123, 14, 63, ...], [117, 255, 245, ...]]
key_confirmed = false
[keys_confirmed]
Code References
| File |
Line |
Description |
libs/hbb_common/src/lib.rs |
300-305 |
get_uuid() fallback to get_key_pair() |
libs/hbb_common/src/config.rs |
893-906 |
Config::get_key_pair() — loads + generates keypair |
libs/hbb_common/src/config.rs |
899-903 |
Background thread writes config via config.store() |
libs/hbb_common/src/config.rs |
500-513 |
store_path() → confy::store_path_perms() (0o600) |
libs/hbb_common/src/config.rs |
615-637 |
Config::path() — directories_next::ProjectDirs resolution |
libs/hbb_common/src/config.rs |
400-427 |
patch() — /root → /home/<user> redirect (skipped for root) |
libs/hbb_common/src/config.rs |
580-584 |
Config::store_() — stores after encryption |
src/common.rs |
107-155 |
gen_sk() — separate keypair mechanism for server |
src/common.rs |
130 |
sign::gen_keypair() — different from Config path |
Impact
- Unexpected file in data directory —
RustDesk.toml is a client config format, confusing for server operators
- Potential key confusion — two keypair storage locations exist (
id_ed25519 vs RustDesk.toml), potentially diverging
- Permissions — file created with
0o600, owned by root (expected for root container)
- Data pollution — this file will appear in any backup/sync of the data directory
Suggested Fixes
Option A: Fix get_uuid() to not depend on Config::get_key_pair() in server context (recommended)
The get_uuid() fallback is designed for client machines where machine_uid may be absent. On a server binary, this fallback is inappropriate. Replace the fallback with a server-appropriate UUID source (e.g., generate once from a static salt + build info):
// libs/hbb_common/src/lib.rs
pub fn get_uuid() -> Vec<u8> {
if let Ok(id) = machine_uid::get() {
return id.into();
}
// Server context: use build-derived static UUID, not Config keypair
crate::get_server_uuid()
}
Option B: Make Config::path() server-aware
Add a server-mode check that redirects ~/.config/rustdesk/ to the current working directory or an explicit server-specific path. Could be triggered by an environment variable like RUSTDESK_SERVER=1.
Option C: Skip Config::load() for server builds
The server binary does not need Config::load() behavior at startup. A compile-time #[cfg(feature = "server")] guard could skip the get_key_pair() fallback in get_uuid() entirely.
Reproduction
- On a Linux host, create an empty
data/ directory
docker run --rm -v $(pwd)/data:/root ghcr.io/rustdesk/rustdesk-server:latest hbbs
- Observe:
data/.config/rustdesk/RustDesk.toml is created
- Check container logs — the
Generated new keypair for id: log line appears
Version References
| Component |
Version / Commit |
| rustdesk-server repo |
815c728837b8a091c9feeeabb423d543be3a7f8d |
| hbb_common submodule |
83419b6549636ee39dacef7776c473f5802e08d6 |
| Docker image (investigated) |
ghcr.io/rustdesk/rustdesk-server@sha256:10818ec05b179039c6660f4d8e74b303f0db2858bbad2b18e24992ea22d54cd6 |
Report generated: 2026-05-13
Investigated by: AI Agent
Repos examined: rustdesk-server (local clone), rustdesk (local clone, pre-pulled)
(Generated by AI Agent)
Summary
When
hbbs(RustDesk rendezvous server) starts inside a Docker container with a bind-mounted data directory (./data:/root), it inadvertently creates a~/.config/rustdesk/RustDesk.tomlfile inside the data directory. This file is the same client-side config format used by RustDesk desktop/mobile clients and should not exist in the server's working directory.The file is created by
hbb_common(a shared library used by both client and server), specifically throughConfig::get_key_pair()being called at startup via a fallback path inget_uuid().Affected Component
hbbs(rendezvous server)hbbr(relay server) — does not usehbb_common::Configstartup chain that triggers config writelibs/hbb_commonin bothrustdesk-serverandrustdeskreposEnvironment
volumes: ./data:/root(bind mount)/root/docker/rustdesk/data/.config/rustdesk/RustDesk.toml0o600(owned by root)Root Cause Analysis
Trigger chain
The entry point is
check_software_update()in the hbbs main thread, which spawns a worker thread. That worker callsversion_check_request(), which invokesget_fingerprint()— andFingerprintingInfo::new()accessesConfig::get_id(), which triggers theCONFIGlazy_static initializer (and thusConfig::load()).Why
key_pairis written separatelyConfig::get_key_pair()operates independently ofgen_sk(). It:KEY_PAIRcache — misses~/.config/rustdesk/RustDesk.tomlviaConfig::load_("")key_pair.0empty → generatessign::gen_keypair()CONFIG.write()and callsconfig.store()again with the populated keypairResult: two keypair storage mechanisms coexist independently:
gen_sk()(server-native)./id_ed25519RendezvousServer::start()Config::get_key_pair()(shared lib)~/.config/rustdesk/RustDesk.tomlcheck_software_update()→ T1Why the path lands in the data directory
On Linux,
directories_next::ProjectDirs::from("", "", "RustDesk")resolves to:Because Docker is started as root with
volumes: ./data:/root, the host path./data/.config/rustdesk/RustDesk.tomlis created.The
patch()function atconfig.rs:400only redirects/rootto/home/<user>when the effective user is not root — but inside the container the EUID is 0, so the redirect never triggers.Why
get_uuid()callsget_key_pair()on the serverget_uuid()atlib.rs:300is a cross-platform machine identifier used by password security and fingerprinting:On Linux containers,
machine_uid::get()reads/etc/machine-idor/var/lib/dbus/machine-id, which may not exist or may be inaccessible. When it fails, the fallbackConfig::get_key_pair().1executes — invoking the client-side config system unnecessarily in a server context.Notice the timestamp ordering from container logs confirms the call sequence:
get_key_pair()(via check_software_update thread) fires first, followed bygen_sk()(via RendezvousServer::start). The ~1ms gap matches thread scheduling.File Contents
data/.config/rustdesk/RustDesk.tomlcontains the encrypted ID and keypair:Code References
libs/hbb_common/src/lib.rsget_uuid()fallback toget_key_pair()libs/hbb_common/src/config.rsConfig::get_key_pair()— loads + generates keypairlibs/hbb_common/src/config.rsconfig.store()libs/hbb_common/src/config.rsstore_path()→confy::store_path_perms()(0o600)libs/hbb_common/src/config.rsConfig::path()—directories_next::ProjectDirsresolutionlibs/hbb_common/src/config.rspatch()—/root→/home/<user>redirect (skipped for root)libs/hbb_common/src/config.rsConfig::store_()— stores after encryptionsrc/common.rsgen_sk()— separate keypair mechanism for serversrc/common.rssign::gen_keypair()— different from Config pathImpact
RustDesk.tomlis a client config format, confusing for server operatorsid_ed25519vsRustDesk.toml), potentially diverging0o600, owned by root (expected for root container)Suggested Fixes
Option A: Fix
get_uuid()to not depend onConfig::get_key_pair()in server context (recommended)The
get_uuid()fallback is designed for client machines wheremachine_uidmay be absent. On a server binary, this fallback is inappropriate. Replace the fallback with a server-appropriate UUID source (e.g., generate once from a static salt + build info):Option B: Make
Config::path()server-awareAdd a server-mode check that redirects
~/.config/rustdesk/to the current working directory or an explicit server-specific path. Could be triggered by an environment variable likeRUSTDESK_SERVER=1.Option C: Skip
Config::load()for server buildsThe server binary does not need
Config::load()behavior at startup. A compile-time#[cfg(feature = "server")]guard could skip theget_key_pair()fallback inget_uuid()entirely.Reproduction
data/directorydocker run --rm -v $(pwd)/data:/root ghcr.io/rustdesk/rustdesk-server:latest hbbsdata/.config/rustdesk/RustDesk.tomlis createdGenerated new keypair for id:log line appearsVersion References
815c728837b8a091c9feeeabb423d543be3a7f8d83419b6549636ee39dacef7776c473f5802e08d6ghcr.io/rustdesk/rustdesk-server@sha256:10818ec05b179039c6660f4d8e74b303f0db2858bbad2b18e24992ea22d54cd6Report generated: 2026-05-13
Investigated by: AI Agent
Repos examined: rustdesk-server (local clone), rustdesk (local clone, pre-pulled)
(Generated by AI Agent)