← Back to the blog

Building a sandboxed SSH tunnel manager for the Mac menu bar

I run SSH tunnels constantly. ssh -L is easy until you have a dozen of them and can't remember which host and port goes with which. I wanted a small menu bar app that remembers them for me. Everything I found was either subscription-priced or free but clunky and neglected, so I built my own. The first version took an evening of vibe coding with Claude Code. It was simple, just a wrapper around /usr/bin/ssh.

At this point I thought to myself, why not put it up on the App Store, should be easy right? It was not.

Why not just run ssh as a subprocess?

It worked fine, but the Mac App Store requires apps to follow the App Sandbox rules, and launching the system /usr/bin/ssh as a subprocess isn't allowed under them. I looked at three ways around that:

I went with the third. Citadel on top of SwiftNIO SSH was a good starting point: each server is a native SSHClient connection, and each tunnel is a NIO channel listener. It also turned out to be better than a subprocess. The app sees real connection state and auth errors instead of parsing ssh output, which made auto-reconnect straightforward to write myself. It retries every 10 seconds up to a limit you set, and it doesn't retry on auth failures.

Citadel does the heavy lifting: the SSH transport, password and key authentication, OpenSSH private key parsing (including passphrase-encrypted keys), and the channels that carry the forwarded traffic. It's a young library, though, and it only covers part of what OpenSSH does. It doesn't honor ssh_config directives, talk to an agent, or follow jump hosts, so anything beyond a direct connection with a password or key file is something I'd have to build myself. And what it does support had gaps I had to fix.

The sandbox has other restrictions as well:

So the app supports password auth and ed25519, RSA and ECDSA key files. RSA signs with rsa-sha2-256, since OpenSSH 8.8 and later disable plain ssh-rsa (RSA with SHA-1) by default. Getting there meant forking both libraries: swift-nio-ssh needed a way for a key to name its signature algorithm, so RSA could use SHA-2, and Citadel needed the matching RSA signing fix plus ECDSA (P-256, P-384 and P-521) support in its OpenSSH key parser.

Export compliance was the worst part

Shazbot "contains encryption," even though it only does what SSH already does. That meant export compliance paperwork for multiple countries. App Store Connect required it for France too, but it was never clear what needed to be filled out, so I gave up on that one. Sorry, France.

What's next

I'm planning per-server hotkeys and notifications for dropped connections, and I'd like to keep it from growing much beyond that. I'd rather it do one job cleanly. If you try it and something breaks, let me know.

Shazbot on the Mac App Store