Skip to content

hbbs writes client-style config file to data directory via Docker bind mount #653

@xlionjuan

Description

@xlionjuan

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:

  1. Locks KEY_PAIR cache — misses
  2. Loads ~/.config/rustdesk/RustDesk.toml via Config::load_("")
  3. Finds key_pair.0 empty → generates sign::gen_keypair()
  4. 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

  1. Unexpected file in data directoryRustDesk.toml is a client config format, confusing for server operators
  2. Potential key confusion — two keypair storage locations exist (id_ed25519 vs RustDesk.toml), potentially diverging
  3. Permissions — file created with 0o600, owned by root (expected for root container)
  4. 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

  1. On a Linux host, create an empty data/ directory
  2. docker run --rm -v $(pwd)/data:/root ghcr.io/rustdesk/rustdesk-server:latest hbbs
  3. Observe: data/.config/rustdesk/RustDesk.toml is created
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions