2.7.1: Host key verification still uses the client's numeric id — wrong host's key is compared and overwritten, breaking host key pinning on a sync server
Platform
App - macOS (Apple Silicon) desktop against a self-hosted server; the code path is server-side, so every client using "Remote server" origin is affected
Server Installation Method
Docker
Version
Desktop app 2.7.1; self-hosted server 2.7.1
Troubleshooting
The Problem
Follow-up to #1092 / Termix#1178. The connection itself is now correctly resolved by syncId, but host key verification was missed and still uses the raw client-supplied numeric id. On a sync server that id names a different host, so Termix compares the connected machine's key against another host's stored key, shows that other host in the prompt, and — when the user accepts — writes the key into that other host's row.
In src/backend/hosts/terminal/index.ts the resolution is correct:
resolvedHostData = (hostSyncId
? await resolveHostBySyncId(hostSyncId, userId)
: await resolveHostById(id, userId));
but id is never reassigned to resolvedHostData.id, and the two host-key calls keep using it:
const preloadedHostData = await SSHHostKeyVerifier.preloadHostData(id); // ~line 2130
...
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(id, ip, port, ws, userId, false, preloadedHostData), // ~line 2146
ip/port/credentials/jump hosts are all taken from resolvedHostData, so the session goes to the right machine — only the identity check goes to the wrong row.
Reproduced with exact data. My two databases are offset by one for these hosts:
| Host |
Desktop id |
Server id |
Real ed25519 key (hex prefix, via ssh-keyscan) |
| 100 File Browser (192.168.1.100) |
3 |
4 |
…2a77818992bacbce9e167e… |
| 103 AdGuard (192.168.1.103) |
4 |
5 |
…40a82d2487ec2f7a54653c… |
Opening a terminal to 103 AdGuard (desktop id 4) produces a host key prompt titled "100 File Browser:22", showing File Browser's stored key as "previous key" and AdGuard's key as "new fingerprint" — because server row 4 is File Browser.
After accepting, the server rows read:
id=4 100 File Browser 192.168.1.100 fp=…40a82d2487ec… changed=1 <-- AdGuard's key, wrong
id=5 103 AdGuard 192.168.1.103 fp=…40a82d2487ec… changed=0
File Browser is now pinned to AdGuard's key.
Consequences:
- Host key pinning is effectively disabled. Every host is verified against a neighbouring host's pin, so a genuine key change — the thing pinning exists to catch — cannot be distinguished from this bug.
- Accepting corrupts a different host's pin, which makes that host prompt on its next connection. The user sees an endless series of "key changed" prompts that never stick ("I accept it, reconnect, and it asks again"), and each acceptance damages another row.
- Users are trained to click through host key warnings, which is the exact opposite of what the dialog is for.
- The prompt's "previous key" belongs to an unrelated machine, so a user who tries to verify the fingerprint out-of-band cannot make sense of it.
Note this is also why hosts sit at Reachable with no explanation (#1266): the metrics collector is refused by the same mismatched comparison.
How to Reproduce
- Desktop + self-hosted server, Remote Sync on, connection origin "Remote server", and host row ids that differ between the two databases (any sync history produces this).
- Open a terminal to a host whose server-side id differs from its desktop id.
- The host key prompt names a different host and shows that host's stored key as "previous".
- Accept it: the key is written to the other host's row (
hostKeyChangedCount increments there), the connected host's own row is untouched, and the prompts keep coming.
Additional Context
Suggested fix: use the resolved row's id for the host-key path, e.g. reassign after resolution
const effectiveHostId = resolvedHostData?.id ?? id;
and pass effectiveHostId to both preloadHostData() and createHostVerifier(). A regression test asserting that the verifier is constructed with the resolved id (not the client id) when the two differ would lock this down.
Worth auditing the same pattern in the other host-key call sites (hosts/metrics/index.ts builds its verifier from host.id, which is server-side and therefore fine, but the file-manager, docker and tunnel paths deserve a look).
Also: consider giving users a way to reset a host's stored key from the UI. Right now hostKeyFingerprint is written only by the verifier, so a row corrupted this way cannot be repaired from the app.
2.7.1: Host key verification still uses the client's numeric id — wrong host's key is compared and overwritten, breaking host key pinning on a sync server
Platform
App - macOS (Apple Silicon) desktop against a self-hosted server; the code path is server-side, so every client using "Remote server" origin is affected
Server Installation Method
Docker
Version
Desktop app 2.7.1; self-hosted server 2.7.1
Troubleshooting
The Problem
Follow-up to #1092 / Termix#1178. The connection itself is now correctly resolved by
syncId, but host key verification was missed and still uses the raw client-supplied numeric id. On a sync server that id names a different host, so Termix compares the connected machine's key against another host's stored key, shows that other host in the prompt, and — when the user accepts — writes the key into that other host's row.In
src/backend/hosts/terminal/index.tsthe resolution is correct:but
idis never reassigned toresolvedHostData.id, and the two host-key calls keep using it:ip/port/credentials/jump hosts are all taken fromresolvedHostData, so the session goes to the right machine — only the identity check goes to the wrong row.Reproduced with exact data. My two databases are offset by one for these hosts:
ssh-keyscan)…2a77818992bacbce9e167e……40a82d2487ec2f7a54653c…Opening a terminal to 103 AdGuard (desktop id 4) produces a host key prompt titled "100 File Browser:22", showing File Browser's stored key as "previous key" and AdGuard's key as "new fingerprint" — because server row 4 is File Browser.
After accepting, the server rows read:
File Browser is now pinned to AdGuard's key.
Consequences:
Note this is also why hosts sit at
Reachablewith no explanation (#1266): the metrics collector is refused by the same mismatched comparison.How to Reproduce
hostKeyChangedCountincrements there), the connected host's own row is untouched, and the prompts keep coming.Additional Context
Suggested fix: use the resolved row's id for the host-key path, e.g. reassign after resolution
and pass
effectiveHostIdto bothpreloadHostData()andcreateHostVerifier(). A regression test asserting that the verifier is constructed with the resolved id (not the client id) when the two differ would lock this down.Worth auditing the same pattern in the other host-key call sites (
hosts/metrics/index.tsbuilds its verifier fromhost.id, which is server-side and therefore fine, but the file-manager, docker and tunnel paths deserve a look).Also: consider giving users a way to reset a host's stored key from the UI. Right now
hostKeyFingerprintis written only by the verifier, so a row corrupted this way cannot be repaired from the app.