rustdesk / rustdesk/rustdesk-server
hbbs writes client-style config file to data directory via Docker bind mount
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 10.4k
- Forks
- 2.6k
- PR merge metrics
- No merged PRs in 30d
Description
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 usehbb_common::Configstartup chain that triggers config write - Library:
libs/hbb_commonin bothrustdesk-serverandrustdeskrepos
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_PAIRcache — misses - Loads
~/.config/rustdesk/RustDesk.tomlviaConfig::load_("") - Finds
key_pair.0empty → generatessign::gen_keypair() - Spawns a background thread that acquires
CONFIG.write()and callsconfig.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.tomlis a client config format, confusing for server operators - Potential key confusion — two keypair storage locations exist (
id_ed25519vsRustDesk.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.tomlis 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)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with libs/hbb_common/src/lib.rs, especially get_uuid(), and trace its fallback into Config::get_key_pair() in libs/hbb_common/src/config.rs. Compare that path with src/common.rs:107-155 and the hbbs startup chain through check_software_update(). Reproduce with the documented Docker bind mount and verify that hbbs no longer creates .config/rustdesk/RustDesk.toml while retaining its server key behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, rust
- Domain
- backend, devops
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100