Repository navigation
spin: connect with this machine's OpenSSH, which negotiates ML-KEM - #8
Merged
Merged
Conversation
A workspace's sshd will accept one key exchange, mlkem768x25519-sha256 (spin-stack/spin-machine#245), and ssh2 has no ML-KEM, so the connection is the system's ssh: - one ControlMaster, carried by `spin tunnel <session>` as its ProxyCommand: the session is carried once, so everything rides on that login - the server's start as an exec through it, and each connection VS Code makes to the server as `ssh -W` through it; - held to the claim's host key as the one line of a known_hosts of its own, StrictHostKeyChecking yes, no global file; logged in with the certificate alone (CertificateFile, IdentitiesOnly, BatchMode); -F /dev/null, so nothing of the person's config changes what it is; and -o KexAlgorithms=mlkem768x25519-sha256; - `ssh -V` is asked first, and an OpenSSH older than 9.9 refused in VS Code before the CLI is run, naming the version found, the one needed and why, as the spin CLI does. An unreadable banner is let through with a line in the log. Windows is refused up front: there is no spin CLI for it, and OpenSSH for Windows has no ControlMaster. LogLevel is INFO, not ERROR: ssh says "no matching key exchange method found" at INFO, and that refusal is the one this change is about. ssh2, its types, its patch and the test of its internals go. The tests run OpenSSH on both ends: a real sshd set up as a workspace's (certificates only, ML-KEM alone), which the connection logs in to, refuses another principal's certificate and another host key, and cannot negotiate with an sshd that has no ML-KEM; the resolve test drives the real ssh through a wrapper that records each run and logs in as the test's user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aledbf
marked this pull request as ready for review
November 13, 2026 18:56
aledbf
added a commit
that referenced
this pull request
Nov 14, 2026
2.0.0 connects with ssh2, whose key exchange the workspace's sshd no longer offers from spin-machine v20261113.03 on (spin-machine#245: mlkem768x25519-sha256 alone), and spin main already pins v20261114.02: the next spin release would open no spin+ workspace with 2.0.0. 2.1.0 carries #6 (spin ssh-session and spin tunnel) and #8 (the system OpenSSH, which negotiates ML-KEM). test:e2e passed against spin's e2e stack at f15122137 (spin-machine v20261114.02) with VS Code 1.146.0 (69047191fc42): 8 tests, open, reconnect, stopped, key not enrolled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The workspace image's sshd will accept one key exchange,
mlkem768x25519-sha256(spin-stack/spin-machine#245): post-quantum, and a login costs what curve25519's does (40 vs 39 ms on the bench fleet, where sntrup761x25519 took 104). ssh2 has no ML-KEM, so Spinbox Remote could not connect to such a workspace. This moves the connection to the machine's own OpenSSH. spin's side is https://github.com/spin-stack/spin/pull/1206.What changes
ssh(src/spin/connection.ts): one ControlMaster (ssh -M -N), carried byspin tunnel <session>as its ProxyCommand.spin ssh-sessionhands out a session thatspin tunnelcarries once, so a second login would need a second session, certificate and claim check. Everything after the login goes through that one connection: the server's start runs as an exec through it, and each connection VS Code makes to the server is anssh -W 127.0.0.1:<port>through it, behind the same loopback listener as before. Readiness isssh -O check. Closing it kills the master, which ends the tunnel and every client.hostVerifierwas. The claim's key, parsed and re-encoded byparseHostKey, is the one line of a known_hosts that belongs to this connection alone (mkdtemp, 0600), withHostKeyAlias,StrictHostKeyChecking=yes,GlobalKnownHostsFile=/dev/nullandUpdateHostKeys=no. Login uses the certificate alone:-i <identity>,CertificateFile,IdentitiesOnly,PreferredAuthentications=publickey,BatchMode.-F /dev/nullkeeps the person'sHost *(a ProxyJump, a ControlMaster, a forwarded agent) from changing the connection. Every run passes-o KexAlgorithms=mlkem768x25519-sha256.src/spin/openssh.ts):ssh -Vis asked before the CLI is run. An OpenSSH older than 9.9 is refused in VS Code as a modalSpinRefusal(ssh_outdated, not temporary). The message names the version found, the version needed and the reason, in the same words as the spin CLI. An unreadable banner is let through with a line in the log. A missingsshis refused asssh_not_found. A successful check is cached per PATH; a refusal is not cached, so updating OpenSSH and opening again works without reloading.no matching key exchange method foundat INFO, andERRORwould hide exactly that refusal.platform_unsupported). There is no spin CLI for Windows (spin builds linux and darwin; vscode-spinbox carrieslinuxanddarwinonly), CI is Linux-only, and OpenSSH for Windows has no ControlMaster. A per-connection design without a master (ssh -N -L) would need a freshspin ssh-sessionfor each login, because a session is carried once.@types/ssh2,@types/ssh2-streams,patches/ssh2@1.17.0.patch, itspatchedDependenciesandallowBuilds, its dependabot ignore, the esbuild externals andtest/ssh2.security.test.tsare gone.keys.tskeeps onlyparseHostKey.Tests
OpenSSH runs on both ends, and the runner needs OpenSSH 9.9 or newer; CI's
ubuntu-latestis 26.04 with 10.2.test/sshd.ts: a real sshd set up as a workspace's (certificates only,KexAlgorithms mlkem768x25519-sha256).test/spin/connection.sshd.test.ts: logs in with ed25519 and ecdsa certificates; refuses the bare key, another principal's certificate, a host key that is not the pinned one, and an sshd without ML-KEM (no matching key exchange method found). Also carries a loopback port through-W. I plantedStrictHostKeyChecking=noandKexAlgorithms=+…, and the host-key and ML-KEM tests failed.test/spin/openssh.test.ts: the version gate (9.6, 9.8, Windows 9.5 and 8.9 refused with the message; 9.9, Windows 9.9, 10.2 and 9.10 pass; Dropbear and an empty banner warn), the PATH lookup and re-check after a refusal, and the options: KexAlgorithms, ProxyCommand, known hosts, strict checking, certificate,-F /dev/null,%escaping.test/spin/resolve.test.ts: activation through to the server's answer. The realsshruns through a wrapper that records each run and logs in as the test's user (a test sshd admits only its own user). Every run must carry the KexAlgorithms and strict host key options. A new case checks that an OpenSSH 9.6 is refused, with the exact message shown, before the CLI is asked anything.go tool task cipasses locally (gate, install, lint, typecheck, 91 tests, package). The e2e lane (task test:e2e, against spin's KVM stack) was not run.test/e2e/spin.e2e.test.tswas only updated forforwardOut(port).🤖 Generated with Claude Code