Repository navigation
fix(cli): create SSH keys locally and register only the public key - #231
Merged
Merged
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
…eb UI does Co-authored-by: Cursor <cursoragent@cursor.com>
k223kim
marked this pull request as ready for review
November 4, 2026 14:53
k223kim
requested review from
ethanwharris,
justusschock,
rusenask and
tejapulagam
as code owners
November 4, 2026 14:53
Earlier tests in the cli/ suite can leave rich's console in color mode, so the error box's borders arrive wrapped in ANSI codes and split the message. Co-authored-by: Cursor <cursoragent@cursor.com>
justusschock
approved these changes
Nov 4, 2026
tejapulagam
approved these changes
Nov 4, 2026
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 CLI now sets up SSH the same way the web UI's setup script does: the key is created on the user's machine with
ssh-keygen, and its public key is registered withCreateSSHPublicKey(POST /v1/ssh-keys) through the SDK's generated client, which sends thelightning logintoken asAuthorization: Bearer.What changed
All three commands now share
ensure_ssh_key()incli/utils/ssh_connection.py:~/.ssh/lightning_ed25519is missing, it runsssh-keygen -t ed25519 -N "" -C lightning@<hostname>. If only the.pubfile is missing, it recreates it withssh-keygen -y. Once~/.ssh/lightning_ed25519is there, it's never replaced.CreateSSHPublicKeyon every run, naming the key after its comment (lightning@<hostname>by default), as the web UI does. The CLI doesn't check the account's keys first: the server returns a key the account already has, unchanged and without a new row, even at the key cap, and compares by fingerprint. The CLI prints nothing about registration, because a new key and a repeat look the same. The create isn't retried, because once the client's retries run out it raises a bareExceptionthat drops the server's message. If the server refuses, the CLI shows its message as is, for example the per-user key limit.Per command:
lightning ssh configureand the legacylightning configure sshrun the steps above, then add aHost <studio name>entry to~/.ssh/config. They skip this when aHostorMatchblock for that name already has.ssh/lightning_ed25519as anIdentityFile. That check is a port of the web UI'sawkcheck. Before, the check was a substring match onHost <name>, which also matchedHost <name>-2and left an entry pointing atlightning_rsaalone.--overwriteno longer replaces anything. It prints a note saying to delete the key to make a new one, and its help text says it has no effect.lightning studio sshruns the steps above and thenssh -i ~/.ssh/lightning_ed25519 s_<id>@ssh.lightning.ai. Before, it used a different key,~/.lightning/lightning_rsa. The "retry with fresh keys" branch is removed because there's nothing left to refresh. Ifsshitself is missing, the command now raises aClickExceptioninstead of aRuntimeError.-ovalues are passed as single arguments, so-o "StrictHostKeyChecking no"now works.auth.api_key or auth.load()) is removed from all three commands. It refused users signed in with only the CLI login token.require_auth_header()still stops them before any key work when there are no credentials.lightning ssh generateandlightning generate sshprint the same entry, which now points atlightning_ed25519.lightning job ssh,lightning mmt sshandlightning studio connectuse the same helper, so they get the new key too. Injob sshandmmt sshthe only change is that theforce_downloadretry is gone. Jobs are out of scope here, and their connect path wasn't checked.Matching the web UI's setup script
The CLI uses the same key file (
~/.ssh/lightning_ed25519), comment (lightning@<hostname>), default name, and~/.ssh/configlines, indented by two spaces. This means the browser flow and the CLI share one key on each machine. The entry checks the gateway's host key, as the script's does:StrictHostKeyChecking accept-newwithUserKnownHostsFile ~/.ssh/lightning_known_hosts, sosshrecords the key on the first connection and refuses a changed one after that. There are two differences:ssh-keygenask for one. The CLI passes-N ""so thatlightning studio sshworks without a terminal, as the oldlightning_rsakey did. A key that the script already made with a passphrase is used as is. If its.pubfile is missing,ssh-keygen -yasks for the passphrase on the terminal.Host ssh.lightning.ai. The CLI writesHost <studio name>, withUser s_<id>andHostname ssh.lightning.ai, as it did before, so thatssh <studio name>keeps working. The port is always 22, so there is noPortline.Old
lightning_rsaentriesA machine set up before this change may still have a
Host <studio name>block whose onlyIdentityFileislightning_rsa. That's whylightning ssh configureadds the new entry after it instead of treating the host as set up.sshuses everyIdentityFilefrom every block that matches, even withIdentitiesOnly yes, so it offers both keys. I checked this withssh -G(OpenSSH 9.9), and a test checks it too. The old block comes first, so itsUserstill wins. That's the same Studio unless a Studio name was reused for a different Studio.How the key is generated
The CLI runs
ssh-keygenrather than using a Python library:cryptographyfor every SDK user.ssh, andssh-keygenships with it: OpenSSH on Linux and macOS, and the OpenSSH client that Windows 10 (1809 and later) and 11 install by default.subprocesspasses-N ""to the Windowsssh-keygenas an empty argument. Ifssh-keygenisn't found, the CLI says "ssh-keygen was not found. Install OpenSSH and run this again." and registers nothing.ssh-keygencan't prompt. If another process creates the key between the existence check and thessh-keygencall,ssh-keygenrefuses to overwrite it.Testing
Unit tests: what each new test checks, and what was run
tests/api/test_ssh_key_api.pycall the real generated client, mocked only at urllib3. They check that the create isPOST /v1/ssh-keyswith{name, publicKey}. They check that the create sendsAuthorization: Bearer <login token>, that a refusal carries the server'smessage, and that a 500 on create isn't retried.tests/cli/utils/test_ssh_connection.pyuse the realssh-keygenin a temporary home directory. They check that the key is ed25519 with no passphrase, mode 0600 and commentlightning@<hostname>. They also cover these cases:~/.ssh, such as one the web UI's script made, is registered without being replaced..pubfile is recreated.ssh-keygenis explained.tests/cli/ssh/test_configure.pycovers these cases:--overwrite, keeps the key, registers it again and leaves the config alone.ssh -Greads a new entry asStrictHostKeyChecking accept-newwith the Lightning known-hosts file.lightning_rsa-only entry, andssh -Gthen offers both keys.Host/Matchcheck works.tests/cli/studio/test_ssh.pychecks the exactsshcommand, including-ovalues, and that both runs register the same key. The job and MMT retry tests are updated for the removed retry.tests/api/test_ssh_key_api.py,tests/cli/utils/test_ssh_connection.py,tests/cli/ssh/,tests/cli/studio/test_ssh.py,tests/cli/studio/test_connect.py,tests/cli/job/test_ssh.py,tests/cli/mmt/test_ssh.py(78 passed), andtests/cli/test_entrypoint.pywithtests/cli/test_temporarily_unavailable.py(216 passed).pre-commit runpasses on the changed files, including ruff, ruff-format and mypy.End to end, against a local control plane. The CLI from this branch was run against a local control plane built from the latest platform
master, with the SSH gateway onlocalhost:2222and a Studio running. The CLI signed in with a local session inLIGHTNING_AUTH_TOKEN, sent asAuthorization: Bearer. Every case below passed.Demo: https://www.loom.com/share/6c229fdb9e5c4a1d91bb03d41110a0ee
Cases and what each one checked
lightning studio sshand the~/.ssh/configentry always namessh.lightning.ai, so each connection added-o HostName=localhost -o Port=2222.~/.ssh/lightning_ed25519,lightning ssh configurecreated an ed25519 key with mode 0600 and the commentlightning@<hostname>. The account gained oneusers.ssh_keysrow, holding exactly that public key.lightning studio sshthen opened a shell in the Studio, and the gateway logged the session.configureagain printed no "Created SSH key", kept the same fingerprint and added no row.ssh-setup.shmade was registered byconfigurewithout being replaced: the fingerprint and the file's modification time didn't change. It then connected. A second script-made key was pasted into the web UI's SSH dialog first. Runningconfigureafterwards left it as one row with the same ID.ssh ssh-ui-reviewsaved the gateway's host key to~/.ssh/lightning_known_hostson the first connection and connected without a prompt on the second. With that line replaced by a different key,sshrefused to connect.lightning_rsaentry. With aHost ssh-ui-reviewblock naming onlylightning_rsain~/.ssh/config,configureadded the new entry after it, and the alias connected.configureexited with code 1 and printed "Could not add the SSH key … to your Lightning account:" followed by the server's message.~/.ssh/configwas unchanged.DELETE /v1/ssh-keys/{id}, plainsshwas refused withPermission denied (publickey). Runninglightning studio sshafterwards registered the key again and connected, because the CLI registers on every run. A key deleted in the web UI therefore comes back on a machine that runslightning studio sshagain.Not tested:
.pubfile is missing (the interactivessh-keygen -yprompt) wasn't run.— Claude Opus 5.5, on direct instruction; reviewed before posting