Skip to content

feat(macos): connect to Kit servers over SSH (phase D) - #252

Merged
akonwi merged 10 commits into
mainfrom
fullstack/multi-server-ssh
Oct 22, 2026
Merged

akonwi merged 10 commits into
mainfrom
fullstack/multi-server-ssh

Conversation

@akonwi

@akonwi akonwi commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Phase D of the multi-server project (#186): Kit for Mac connects to Kit servers on other machines over SSH.

What changes

  • Connector (in Swift): the app runs /usr/bin/ssh itself; no kit is bundled or used on this Mac.
    • Each host gets its own ControlMaster (-M -S, ControlPersist=no, RemoteCommand=none). The control socket lives in a 0700 cache directory.
    • The app reads the host's server with kit server status --json and forwards a free loopback port with -O forward.
    • Exit codes 255/127/2/1 map to ssh failed, Kit not installed, Kit too old, and server stopped.
  • Askpass bridge: the app's own executable is SSH_ASKPASS (KitMain runs the helper first). It asks over a per-process Unix socket, with a per-attempt nonce and a getpeereid check.
    • ssh's questions appear in the S9 sheet: host key, password, passphrase, keyboard-interactive, confirm.
    • A cancelled or unanswerable question ends ssh before the refusal reaches it, so ssh never sends an empty password.
  • Background connections never ask. A password or keyboard-interactive question ends the attempt, and the server shows Waiting to sign in. A locked key's passphrase is refused, and ssh goes on to try other keys.
    • Opening the server (Sign In… or its tab) connects again and shows S9.
  • Starting servers: only Add Server and Start Server run kit server start on the host. Reconnects and app launch never start one.
  • Host keys: a changed host key is refused (S16). Kit shows both fingerprints and the ssh-keygen -R command to run in Terminal. StrictHostKeyChecking no/off in the user's config is overridden to ask.
  • Saved answers:
    • Passwords can be saved in Kit's own Keychain item (off by default). They're read without UI and sent only to the account ssh logs in to, never to a jump host.
    • Passphrases are saved through ssh-add --apple-use-keychain (on by default). ssh-add is given the passphrase once.
  • Settings › Servers: SSH rows have Sign In… and Start Server. Pair a Device asks for the URL other devices use (S12), like kit pair --url.
  • Docs: ADR 0045 is updated, docs/features/attach.md gains "In Kit for Mac", and the README is updated.

Design decisions (approved by the user)

  • Option 1: a Swift connector with system ssh.
  • A background question cancels, with no background retries.
  • The app keeps its own ControlMaster.
  • No ssh-add --apple-load-keychain at launch.
  • StrictHostKeyChecking is forced to ask.

Validation

  • xcodebuild … test: 953 tests pass (several full runs).
  • Two code-reviewer passes; all blockers and should-fix items are resolved.
  • Live test against a disposable exe.dev VM running Kit 0.49.0 (protocol 48), with Kit Dev on an isolated home (channel ms-ssh):
    • The S9 host-key sheet appeared. Cancel left "Waiting to sign in".
    • After that, Sign In… showed the sheet again, and Connect led to "Server stopped".
    • Start Server got it to Connected.
    • After I killed the master, it reconnected silently.
    • A launcher tab worked on the VM.
    • Pair a Device made a one-time link.
    • Remove ended the master.
    • The live test found OpenSSH 10.3's fingerprint is: SHA256:… format, now covered by tests with ssh's exact output.
    • The VM, the known_hosts entry, and the isolated home were removed afterwards.
  • Not tested live: password, 2FA and changed-host-key flows. Unit tests cover them with OpenSSH's exact text.

Follow-ups

No core/API changes: only macOS code and docs.

Closes #186

akonwi added 10 commits October 22, 2026 09:05
The app reaches a saved SSH server itself (ADR 0045, option 1): one master
ssh per host, shared by every window, reads kit server status --json and
holds a loopback forward to the server. ssh asks its questions through the
app's own executable as SSH_ASKPASS, over a private socket.

Background connections never ask: a question only the user can answer
leaves the server waiting for sign-in (S9). A changed host key is refused
and shown with both fingerprints (S10). A stopped server is reported and
started only from Start Server (S16).
Add Server saves an SSH host and connects, starting its Kit server as kit
attach does, with ssh's questions as sheets in Settings (S6, S9). Settings
rows offer Sign In… and Start Server. Pair a Device on an SSH server asks
for the URL other devices use, as kit pair --url does (S12).
…ned ones

Review fixes for the SSH connector:
- A cancelled or unanswerable question ends ssh before it is refused, so
  ssh never sends an empty password in its place.
- A question is withdrawn when ssh exits or its window goes away, so it
  can't hold up the host's other connections.
- A saved password goes only to the account ssh logs in to, never a jump
  host, and is read without a Keychain dialog.
- Closing a host ends a master that connects meanwhile. Another copy's
  master keeps its forward, and a busy socket is left alone.
- A dropped connection forgets the transport for its freed local port.
  Connections stop watching hosts they no longer use.
- ssh-add gets a passphrase once.
- The host-key question must be ssh's own. Format characters are dropped
  from host text.
- Each copy of the app has its own askpass socket.
- RemoteCommand from the user's config doesn't replace Kit's commands.
- Add Server explains when a new SSH server didn't connect.
- A background connection refuses a passphrase, host-key or confirm
  question and lets ssh go on, as ssh skips the key or declines; only a
  password or keyboard-interactive question ends ssh, which would
  otherwise send an empty answer. A host-key change is reported before
  a refused question.
- A closed host withdraws its open question, and ends a forward opened
  meanwhile.
- The saved password is read with the legacy keychain's dialog refused.
…ass sockets

Found in the live test against OpenSSH 10.3:
- ssh writes "key fingerprint is: SHA256:…" with a colon and no period,
  which wasn't read as a host key question.
- A changed key's warning may not say how to remove it; the file comes
  from the "Offending … key in" line instead.
- An askpass socket left by a copy of the app that exited is removed when
  the bridge starts.
@akonwi akonwi added the macos Native macOS app (apps/macos) label Oct 22, 2026
@akonwi
akonwi merged commit 946c470 into main Oct 22, 2026
6 checks passed
@akonwi
akonwi deleted the fullstack/multi-server-ssh branch October 22, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macos Native macOS app (apps/macos)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSH servers in the macOS app

1 participant