Skip to content

Releases: GigaionLLC/RD-API-Server

RD-API-Server v1.7.2 — PHP 8.5.12 security release

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 21 Oct 19:09
b47126a

Security release: PHP 8.5.12 (critical, update now)

The runtime image moves to the official PHP 8.5.12 (php:8.5.12-fpm-bookworm, digest-pinned), which fixes two critical, unauthenticated remote code execution vulnerabilities in PHP itself:

  • CVE-2026-109618: out-of-memory handling can leave request data half-initialized; request cleanup then treats it as valid.
  • CVE-2026-109619: request-owned state can outlive its memory and be used by the next request on the same PHP-FPM worker.

Every PHP-FPM installation serving web requests was affected. See the PHP security advisory.

Defense in depth from the advisory: fatal_error_backtraces = 0 in the runtime image, next to session.upload_progress.enabled = Off from v1.7.1.

Upgrade: pull ghcr.io/gigaionllc/rustdesk-api-server:1.7.2 (or :latest) and recreate the container. No database migrations and no configuration changes; the RustDesk client protocol is unchanged.

v1.7.1

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 21 Oct 03:03
01f69c1

Security: PHP session.upload_progress is turned off in the runtime image. The server never uses it; this removes the entry point of the PHP session upload-progress use-after-free (CVE-2026-109618 / CVE-2026-109619, PHP security release 2026-10-21 14:00 UTC). A follow-up release moves the image to the patched PHP once it is published.

RD-API-Server v1.7.0

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 29 Sep 05:07
b812f0c

Before you upgrade — read this

  1. Back up the database and the application key together. This release encrypts more data
    with the application key (address-book peer passwords/hashes and pending OIDC results, in
    addition to the authenticator secrets encrypted since v1.4). The key is APP_KEY if you set
    one, otherwise the key the container generated and persisted at storage/app/.appkey.
    A database restored without its matching key cannot decrypt those values — address-book
    peers would lose their saved passwords, and authenticator (TOTP) secrets would be unreadable.
    Keep the key and the database in the same backup, and give every API replica the same
    APP_KEY (and APP_PREVIOUS_KEYS when rotating).
  2. Migrations run automatically when the container starts (the entrypoint runs
    php artisan migrate --force, as in earlier releases). Five new migrations run once:
    Migration What it does Reversible
    add_presets_applied_at_to_devices_table Records when a device's enrollment-time presets were applied. Devices that have already reported inventory are marked as done. Yes
    add_approval_to_oauth_sessions_table Adds the OIDC approval-step columns. Yes
    hash_client_and_deploy_tokens Replaces every stored client bearer token and deploy token with its SHA-256 digest in place (tokens already issued keep working), keys pending OIDC logins by a digest of the polling code, and encrypts an undelivered OIDC result. No — see below
    encrypt_address_book_peer_secrets Encrypts address_book_peers.password and .hash with the application key. Idempotent. Yes, while the key is available
    add_two_factor_last_counter_to_users_table TOTP replay protection. Yes
  3. Upgrade in a short maintenance window, not as a mixed-version rolling update. Stop old
    replicas, back up, start one upgraded instance (it migrates), then start the rest. An old
    replica still running after the token migration cannot authenticate anyone (the plaintext token
    column no longer exists), and an old replica would write unencrypted peer secrets.
  4. Downgrading after the upgrade signs everyone out. The token digests cannot be turned back
    into tokens, so rolling back to v1.6.x requires every client to sign in again and every deploy
    token to be recreated. Restore the pre-upgrade backup instead if you must go back.
docker compose pull && docker compose up -d

What changed

Device presets can no longer move an enrolled device (High)

/api/sysinfo accepts the OPTION_PRESET_* values a custom client or rustdesk --assign bakes in
(strategy, device group, address book, displayed name/note). Previously they were re-applied on
every upload and overrode admin assignments, so anyone who could read a machine's RustDesk id
and uuid (any local user) could move it into any strategy — and read that strategy's options,
including default-connect-password and proxy credentials — or into any device group, creating
groups at will.

Now presets are an enrollment-time action, which is how they were documented ("devices
auto-file … on first contact"):

  • They apply on the device's first accepted sysinfo upload only.
  • They fill blanks and never overwrite: an existing direct strategy, a non-default device
    group, or an admin-set name/note is kept.
  • strategy_name and device_group_name are matched against existing strategies/groups;
    unknown names are ignored, and a device can never create a device group.
  • The client still receives SYSINFO_UPDATED as before.

Legitimate enrollment is unchanged: a new machine deployed with a custom client or --assign
still lands in its preset strategy and group. Behaviour change to note: if you relied on a
custom client's device_group_name preset creating a group, create that group in the console
first. To re-file a device that is already enrolled, use the console, the v1 API, or
rustdesk --assign (deploy-token authenticated) — not a new sysinfo upload.

Delegated administrators stay inside their scope (Medium-High)

  • A strategy that is also assigned directly to a device outside a delegated admin's scope is
    no longer considered in scope for that admin.
  • rustdesk --assign / /api/devices/cli and /api/devices/deploy with a delegated admin's
    deploy token now respect that admin's scope: only in-scope strategies, only existing in-scope
    device groups (no group creation), and no adoption of a device owned outside the scope. Full
    administrators' tokens behave exactly as before.
  • Strategy options that point a client at other infrastructure or through a credentialed proxy —
    custom-rendezvous-server, relay-server, api-server, key, proxy-url, proxy-username,
    proxy-password — can now be added, changed or removed only by full administrators (console
    and v1 API). Delegated editors can still save strategies that contain them unchanged.

OIDC device sign-in needs an explicit approval (High impact)

Signing in to RustDesk with an OIDC/OAuth provider now shows an approval page after the
provider sign-in: the requesting device's name, OS, RustDesk ID, the IP address that started the
request, and the time. No token is issued until the person presses Approve sign-in on this
device
(a POST bound to a one-time value in the page and a same-site cookie). "Deny" discards the
request. This stops a phishing link built from an attacker's own sign-in request from silently
handing the victim's token to the attacker's device.

The RustDesk client's polling (/api/oidc/auth-query) and every JSON shape are unchanged — the
client simply keeps waiting until the user approves. Also:

  • /api/oidc/auth is rate-limited per source (20 per minute; IPv6 per /64).
  • The runtime Nginx access log no longer records the query strings of /api/oidc/auth-query and
    the OIDC callback, which carry the polling secret. If you run your own proxy in front, consider
    doing the same there: the protocol fixes these values in the query string.

Credentials are no longer stored in plaintext (Medium)

  • Client bearer tokens and deploy tokens are stored as SHA-256 digests and looked up by digest.
    Existing tokens are hashed by the migration and keep working.
  • New deploy tokens created without an expiry date now expire after
    RUSTDESK_DEPLOY_TOKEN_TTL_DAYS days (default 365; 0 restores never-expiring tokens).
    Existing deploy tokens keep the expiry they had.
  • Pending OIDC logins are keyed by a digest of the polling code; an undelivered result is
    encrypted with the application key.
  • Address-book peer password and hash are encrypted with the application key. The client
    API returns exactly the same values as before. See "Before you upgrade" about the key.

Standalone web viewer removed from the public origin (Medium)

The bare development page /assets/webclient/src/ui/viewer.html now returns 404 from the
runtime image; the in-console Connect flow (which injects the server's own connection
settings) is unaffected. The standalone page, where it is still used for development, always
requires an encrypted, key-verified session and no longer takes the server host or key from the
URL. The injected viewer configuration is also escaped more strictly, and the viewer iframe no
longer requests clipboard-read permission it never used.

Login hardening (Medium / Low)

  • Per-account ceiling: failed password attempts are counted per account name across all
    source addresses (default 20 per 15 minutes, shared by the client API and the admin console) and
    refused beyond that — checked before any LDAP bind, so this server cannot be used to lock
    Active Directory accounts faster than the ceiling allows. A successful sign-in resets the count.
    Tune with RUSTDESK_LOGIN_ACCOUNT_MAX_FAILURES (0 disables) and
    RUSTDESK_LOGIN_ACCOUNT_DECAY_MINUTES.
  • IPv6 callers are rate-limited per /64 instead of per address.
  • No account-state disclosure before a correct password: "Account disabled", "Account not
    verified" and "must sign in via SSO" are shown only after the right password; unknown accounts
    cost the same hashing time as known ones.
  • Console honours email login verification: accounts whose login verification is set to
    email now receive a code by email when signing in to the admin console too (previously only the
    RustDesk client asked for it). Mail must be working for those accounts; if the code cannot be
    sent, sign-in fails closed. Before upgrading, check that mail delivery works for any
    administrator whose login verification is "email"
    ; if an administrator is locked out,
    php artisan rustdesk:admin:reset <username> --generate --clear-2fa (inside the container)
    sets a new password and turns login verification off for that account.
  • TOTP codes are single-use: a code (or an older one) that was already accepted cannot be
    replayed within its validity window, in the client and in the console.

Browser security headers (Low)

Console responses now carry X-Content-Type-Options: nosniff,
Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options: SAMEORIGIN and
Content-Security-Policy: frame-ancestors 'self', plus Strict-Transport-Security on HTTPS
requests only. The in-console viewer (same origin) still works.


New settings

Variable Default Meaning
RUSTDESK_DEPLOY_TOKEN_TTL_DAYS 365 Lifetime of a new deploy token created without an expiry date; 0 = never expires
RUSTDESK_LOGIN_ACCOUNT_MAX_FAILURES 20 Failed first-factor attempts per account (all sources) before refusing; 0 disables
RUSTDESK_LOGIN_ACCOUNT_DECAY_MINUTES 15 Window for the per-account ceiling

TRUSTED_PROXIES=* remains the bundled default. As before, it is safe only when the application
...

Read more

RD-API-Server v1.6.3

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 29 Sep 04:01
e4c5dcc

Why upgrade

Several libraries the server ships had published security advisories:

Package Was Now Advisories addressed
league/commonmark 2.8.2 2.10.3 unsafe-link and on* event-handler filter bypasses in AttributesExtension; multiple parser denial-of-service issues
guzzlehttp/guzzle 7.12.1 7.15.5 host-based checks bypassable with non-canonical hosts, among others
guzzlehttp/psr7 2.12.1 2.13.1 one moderate advisory
laravel/framework 13.16.1 13.33.0 no open advisory; brought current within 13.x

The runtime image moves to PHP 8.5.11 (from 8.5.8) and the bundled Compose to MariaDB
11.8.9
(from 11.8.8) — both patch releases in the same series, still pinned by digest.

Upgrading

Pull the new image and recreate the container as usual:

docker compose pull && docker compose up -d

If you use the bundled db service, the MariaDB patch update is applied when the container is
recreated; no dump/restore is needed within the 11.8 series.

Verification

  • PHPUnit: 699 tests, 3714 assertions — pass
  • Pint and PHPStan — pass
  • composer audit and npm audit — no advisories
  • Full CI gate set (runtime image smoke test on amd64/arm64, browser E2E) on the release PR

RD-API-Server v1.6.2

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 17 Aug 21:03

Fixes clicks landing away from the pointer, and adds two ways to see the mapping.

A click lands where the pointer is drawn

"I can't seem to click in the correct spot compared to the shown mouse."

Two code paths derived the same relationship independently:

How it mapped
Drawing the peer's pointer position × (video.width / display.width) — measured
Sending a click localX ÷ DisplayInfo.scale — assumed

Those are inverses only when scale happens to equal display.width / video.width. Nothing enforced that, and when they differed the pointer was drawn in one place while the click arrived in another — with nothing visible to explain the gap.

Input is now derived as the exact inverse of the drawing, which makes disagreement structurally impossible: wherever the remote pointer sits under yours, a click there arrives there. It also removes any need to interpret what scale means on a given platform, and collapses to a plain offset when the video and the display report the same dimensions — the common case, which is unchanged and pinned by its own test.

Two ways to see it

A remote cursor toggle in the session toolbar draws the peer's own pointer alongside yours. They should sit on top of each other; if they ever do not, that is now visible rather than something to infer from where clicks land.

The pointer mapping appears in the statistics overlay (the Stats button):

pointer  video 1280,800 -> peer 640,400
display  1280x800 at 0,0 · video 2560x1600 · scale 2

That is what "something is off" looks like once it is a number: the display and the video disagree by a factor, and the mapping follows the measured one.

Upgrading

docker compose pull && docker compose up -d

Nothing else.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.6.2

Multi-architecture (AMD64/ARM64), digest sha256:f3ee79d76afaacd31992b11a02a58b1418844c4cc7e30fb9336acbe410832fb3. The latest, 1 and 1.6 tags point at the same manifest.

Verification

699 PHP tests, 259 web-client unit tests, 18 browser tests, Pint and PHPStan.

The inverse relationship is pinned directly: a point is walked through the input mapping and back through the drawing mapping, on a display whose scale deliberately disagrees with the measured ratio, and must return to the pixel it started from. The common case is pinned separately, so the fix cannot introduce an offset into deployments that were already correct.

Not verified against a live peer. The test machine available here is not currently sending video, so the mapping was checked by construction and in tests rather than by clicking on a real remote desktop. If a click still misses after upgrading, the two new readouts are what to send: the pointer line names both coordinate spaces and the ratio between them.


Full notes: docs/releases/v1.6.2.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.

RD-API-Server v1.6.1

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 17 Aug 20:13

Four fixes to the browser remote desktop, three of them reported from live deployments, plus the documentation that would have prevented one of them.

You can see your pointer again

"I don't see a mouse at all while trying to control, but the mouse does work if I click."

The viewer hid the local pointer unconditionally, assuming the peer's cursor would replace it. Two things were wrong with that.

The peer suppresses cursor-position updates toward whoever is sending input — deliberately, so a native client's own pointer does not fight an echoed one. A browser viewer that has also hidden its local pointer is left with a remote pointer that lags or stops, and nothing to aim with.

And the peer sends no cursor at all unless asked, because a native client draws its own.

Mode Local pointer Why
Controlling Always visible It is the one that tracks your hand
Watching, remote cursor present Hidden The remote pointer is the interesting one; two are noise
Watching, no remote cursor Visible Better than none

The viewer now also requests the peer's cursor at login, so there is a remote pointer to show.

A connection in progress can be cancelled

Connecting to a machine that asks its user to accept left the viewer waiting with no way out — Disconnect lives in the toolbar, and the toolbar only exists once a session is established.

Cancel now appears for the duration of the negotiation. A deliberate cancel is not retried: closing the socket surfaces as a transport failure, which the reconnect policy would otherwise treat as a transient drop, turning Cancel into a pause.

A configured WebSocket endpoint is validated

The browser refused the URL: Failed to construct 'WebSocket':
The URL's scheme must be either 'http', 'https', 'ws', or 'wss'.
'rustdesk-hbbs' is not allowed.

RUSTDESK_WS_ID_URL=rustdesk-hbbs:21118 is an upstream pasted into the browser-facing setting. The browser is on someone's laptop; it cannot resolve a container name, and without a scheme it will not construct the socket at all.

Worse, the diagnostics page reported a healthy deployment, because it only checked the value was non-empty. It now names the mistake and the setting to use instead, and an unusable endpoint is no longer handed to the browser — so a deployment whose upstreams are correct keeps working despite a leftover variable.

A monitor change no longer blanks the session header

peer_info is re-delivered when the peer's displays change — docking a laptop, waking a monitor — and that copy carries the display list without the identity fields. Writing it straight to the header turned a working session into peer@undefined · undefined minutes in, which reads as a fault rather than a monitor being plugged in.

Documentation

The two variable pairs are the most common setup mistake, so they are now explained wherever an operator meets them — a table in the README, two labelled options in .env.example, and inline comments in the examples:

Who dials it What goes in it
RUSTDESK_WS_*_UPSTREAM This container, over the Docker network rustdesk-hbbs:21118
RUSTDESK_WS_*_URL The operator's browser, over the internet wss://host/ws/id

examples/simple.docker-compose.yml is new: the whole stack in one file, no .env, nothing optional — change four passwords and two hostnames. The full-stack example keeps the pinned digests and hardening notes; .env.example remains the complete reference.

Upgrading

docker compose pull && docker compose up -d

Nothing else.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.6.1

Multi-architecture (AMD64/ARM64), digest sha256:9dac880e4e441d480dcc13f23e9bd82b06467f43e14849d8d57cebe4af092ea7. The latest, 1 and 1.6 tags point at the same manifest.

Verification

699 PHP tests, 254 web-client unit tests, 17 browser tests, Pint and PHPStan.

Confirmed against a live peer through the published image: connected and encrypted, the peer's identity retained, the local pointer visible while controlling, a remote cursor decoded and drawn alongside it, and Cancel present throughout the negotiation.

Stated plainly: the peer used for that check was sending a cursor but no video frames. The previous release, run against the same peer minutes apart, behaved identically — so this is that machine's state rather than a change here. Video was verified end to end in v1.4.4 and is unchanged in this release, but it was not re-confirmed today.


Full notes: docs/releases/v1.6.1.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.

RD-API-Server v1.6.0

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 17 Aug 03:16

Two additions that make a bug report cheap to file and worth reading.

The version is on screen

It now appears in the sidebar footer on every page. It is the first question on every support thread, and an operator should be able to answer it from wherever they already are rather than hunting for a settings screen.

A support report worth attaching to an issue

System → Support report produces what a maintainer otherwise extracts one question at a time:

Environment Version, Laravel, PHP, OS, web server, environment, debug mode
Configuration What is set and what is not — never the values
Browser remote desktop The full diagnostics report, inline
Database Driver, version, migrations applied, device and user counts
Recent log The last 400 lines

What is removed before it leaves the machine

  • IPv4 and IPv6 addresses, and MAC addresses
  • Hostnames, and whole URLs including their paths
  • Email addresses
  • Base64 key material
  • RustDesk peer IDs
  • Any value whose name suggests a secret — DB_PASSWORD, ADMIN_PASS, "api_key", Authorization: Bearer …

Matching on the name is what catches a password of hunter2, which has no pattern to recognise.

The same value always becomes the same placeholder. <host-1> twice means the same host twice; [redacted] twice means nothing. A report nobody can reason from is one nobody will ask for.

Values that are secret by name are never read in order to be hidden. The report says whether a server key is configured and which half of the pair it is — never what it contains.

What deliberately stays

Versions, error messages, file names, stack frames, log levels, 127.0.0.1, timestamps and counts. A report stripped of everything is just a button nobody presses.

It is shown before it is sent

The page displays the finished text and says plainly that redaction is a large reduction in what escapes rather than a guarantee — the last check on "is this safe to publish" has to be a person who knows their own deployment. It requires the settings.view permission, since it describes that deployment.

Upgrading

docker compose pull && docker compose up -d

Nothing else. No migration, no configuration change.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.6.0

Multi-architecture (AMD64/ARM64), digest sha256:9f521a2fad39ed25176d0151751de8ec95f28a6595889313842a8ea65514d970. The latest, 1 and 1.6 tags point at the same manifest.

Verification

694 PHP tests, 253 web-client unit tests, 15 browser tests, Pint and PHPStan. Ten tests cover the redactor alone.

A report was also generated from the published image with passwords, a key, a peer ID, an email, a customer hostname and two addresses seeded into the log — none of them survived, and the version, PHP, kernel release, log levels and timestamps all did.

Two defects were found by reading a real report while the unit tests were already green, and both are now pinned:

  • A harmless production.DEBUG: pair consumed the whole of a following DB_PASSWORD=hunter2 as its value, so the password was never examined and appeared in the report.
  • A kernel release (6.6.87.2-microsoft-standard) is a valid IPv4 address and was being replaced, removing the one piece of platform information that line existed to carry.

Full notes: docs/releases/v1.6.0.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.

RD-API-Server v1.5.0

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 17 Aug 02:34

Every picker whose list grows with the deployment now searches instead of listing it.

The problem

A fleet is thousands of devices. A directory-synced deployment has as many user groups as the directory does. Rendering either into a <select> builds every row into the DOM on page load — so the page is slowest for exactly the deployments with the most to manage, and the operator can only scroll it, never search for the name they already know.

Device and user pickers already worked this way. This release brings the rest into line:

Picker Was Now
Remote control peer 200 most-recent devices in a <select> Searched, online first
User group (create, edit, bulk bar) Every group in scope Searched
Admin role scoped groups Every group in the deployment Searched, as chips
Group "can access" Every group in scope Searched, as chips
Device group "accessible by" Every group in scope Searched, as chips

Fixed lists — statuses, enum choices, quality presets — stay plain <select>s. A combobox for four options is worse, and the rule recorded in CLAUDE.md says so explicitly.

Multi-value selection

The existing combobox was single-value, which is why three pickers had remained multi-selects. It now supports multiple selections as removable chips, with Backspace on an empty box removing the last, and duplicates refused.

The component emits whichever shape the form already posted — a comma-joined hidden field, or one hidden input per value under an array name — so no controller, request validation or write path changed. That was the point: a conversion that alters the request shape is one that can silently save the wrong thing.

Chips use the existing design tokens; no new colours were introduced.

Fixed

The admin role scope picker showed every group in the deployment. It ran an unscoped query, so a delegate confined to a single group could read the names of every group in the deployment from the role form. Group names are not the most sensitive thing in the system, but a scope boundary that leaks the shape of what lies outside it is still a leak. It now goes through AdminScopeService like every other group query.

A dead query on every device-index page load. Every visit fetched every in-scope user, ordered them by name, and rendered none of them — the owner picker had been converted to a combobox and the query was left behind. On a large deployment it was the single biggest unbounded fetch on the page.

Upgrading

docker compose pull && docker compose up -d

No migration, no configuration change, and no request or API shape changed.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.5.0

Multi-architecture (AMD64/ARM64), digest sha256:5b853a9e93e82a7e6ef24af656384319335b1c7434f234dedc92dab64eb53d99. The latest, 1 and 1.5 tags point at the same manifest.

Verification

677 PHP tests, 253 web-client unit tests, 15 browser tests, Pint and PHPStan.

The multi-value combobox was also exercised in a real browser against the published image: searching, selecting, refusing a duplicate, removing a chip, and submitting — with the saved row read back out of the database to confirm the round trip. The PHP suite proves the request shapes; only a browser shows that the chips and their hidden inputs stay in step.


Full notes: docs/releases/v1.5.0.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.

RD-API-Server v1.4.5

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 17 Aug 00:42

This server can no longer hand out a private key, however it is configured.

Why it could

The API server never signs anything. It holds the RustDesk server key for one purpose: giving it to clients and to the browser viewer, which use it to verify that the server signing a handshake is the one they were configured for. Verification needs the public half and nothing else — a search of the codebase finds no signing call at all, and the key is read in exactly two places, both of which hand it out.

It was nonetheless possible to configure the wrong half. An Ed25519 signing key is 64 bytes — a random seed followed by a copy of the 32-byte public key — and RustDesk writes the two side by side:

id_ed25519.pub 32 bytes. Belongs in RUSTDESK_PUBLIC_KEY; goes to clients
id_ed25519 64 bytes. Stays on the server; never distributed

Point the configuration at the second, and this server published — to every client that requested a configuration — the key whose entire purpose is to prove the server's identity. Anyone holding it can impersonate the ID server to every client on the deployment.

Found on a live deployment: hbbs refused a connection presenting it, which is the only reason it surfaced.

What changed

Both distribution paths now go through one service that yields only the public half, deriving it when a private key is supplied.

Deriving rather than refusing is deliberate: an operator who made this mistake gets a working deployment and a loud diagnostic instead of a broken one and a puzzle, and hbbs rejects a client presenting a private key regardless, so passing it through helped nobody. The diagnostics page still reports the misconfiguration in full, prints the correct public key, and says to rotate.

A value that is neither half is dropped rather than distributed — a malformed key breaks every client with an error none of them can diagnose, while no key merely leaves the deployment unauthenticated, which is also reported.

If your configuration holds the private key, rotate the pair. Correcting the setting does not un-distribute what has already been handed out.

RUSTDESK_PUBLIC_KEY

A clearer name for the same setting, since the two filenames are nearly identical:

RUSTDESK_PUBLIC_KEY=<contents of id_ed25519.pub>
RUSTDESK_PUBLIC_KEY_FILE=/path/to/id_ed25519.pub

RUSTDESK_KEY and RUSTDESK_KEY_FILE keep working indefinitely. They are the conventional spelling across the RustDesk ecosystem, and breaking them would fail silently — an unset key disables peer verification rather than raising anything, so an upgrade would quietly downgrade security. The explicit name wins when both are set.

Upgrading

docker compose pull && docker compose up -d

No migration, no configuration change. Renaming the variable is optional.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.4.5

Multi-architecture (AMD64/ARM64), digest sha256:ef622af00af85e3f976973d0bd7ffcb95b8cfd649b9c544948c9d1b08616b62c. The latest, 1 and 1.4 tags point at the same manifest.

Verification

668 PHP tests, 253 web-client unit tests, 15 browser tests, Pint and PHPStan. Six new tests cover the key service directly: that a private key never leaves as one however it is configured, that a key file pointed at id_ed25519 is handled the same way, that a malformed value is dropped rather than distributed, and that the conventional environment name still resolves.


Full notes: docs/releases/v1.4.5.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.

RD-API-Server v1.4.4

Choose a tag to compare

@TheJakeBraun TheJakeBraun released this 16 Aug 23:40

The browser remote desktop works. This release fixes the last of three faults that shared one symptom, and is the first verified end to end against a live peer.

The viewer could not load its own scripts

Its HTML is served from an application route, not from the directory the file lives in, and relative URLs resolve against the document's address. src="./viewer.js" on /admin/remote/frame asked for /admin/remote/viewer.js, which does not exist. One 404 stops the entire module graph — nothing runs, and the page renders as bare HTML: a manual connection form asking for an ID server and a base64 key that had been injected correctly a line above.

The served document now carries a <base> pointing at the published assets.

The trailing slash on that base is load-bearing, and Laravel's asset() strips it. Without one, the last path segment is treated as a filename and every relative URL still resolves a directory too high — the same 404, from a <base> that looks correct. That variant was caught only because the fix was checked in a browser rather than assumed.

Verified end to end

For the first time, against a live peer through the published image:

Rendezvous and relay Connected over WebSocket to hbbs and hbbr
Handshake Encrypted; the plaintext-downgrade warning stayed hidden
Login user@host · Windows reported by the peer
Video H.265, 1920×1080, frames received = frames painted, no drops
Monitors All four of the peer's displays enumerated
Input None sent — the session was run view-only throughout

The session started when Connect was pressed, and not before.

A check for the wrong half of the key pair

An Ed25519 signing key is 64 bytes — a seed followed by the 32-byte public key — and RustDesk writes both halves to disk with confusingly similar names. RUSTDESK_KEY wants the public one.

Configuring the private half is not a cosmetic typo. That value is handed to every client and to the viewer, so the key whose entire purpose is to prove the server's identity gets published to anyone who requests a client configuration — and anyone holding it can impersonate the ID server to every client on the deployment. It also does not work: hbbs compares the presented key against its own and refuses with a licence mismatch.

The diagnostics page now recognises a 64-byte value, says what it is, prints the correct public key extracted from it, and says to rotate the pair. A value that is neither 32 nor 64 bytes of base64 is reported too.

Found by configuring a test deployment from a real one and watching hbbs refuse it.

Upgrading

docker compose pull && docker compose up -d

No migration and no configuration change. Then open System → Remote diagnostics and press Test from this browser — the browser, not the container, is what has to get through your reverse proxy, so it is the one check this server cannot make on your behalf.

Image

ghcr.io/gigaionllc/rustdesk-api-server:1.4.4

Multi-architecture (AMD64/ARM64), digest sha256:8b3561625c8d09bec29c2912fb2fd7cfb371a22ed43209322fdefa7f3e2d6254. The latest, 1 and 1.4 tags point at the same manifest.

Verification

662 PHP tests, 253 web-client unit tests, 15 browser tests, Pint and PHPStan. The new regression test loads the real viewer route and resolves every URL the served document references the way a browser would; it was confirmed to fail without this fix before being kept.


Full notes: docs/releases/v1.4.4.md

RD-API-Server is an independent, third-party, open-source project. It is not affiliated with, endorsed by, or sponsored by RustDesk or Purslane Ltd.