I built sshelf, a little terminal UI for managing and connecting to SSH hosts. Think of it as k9s for ssh: you save a host once, then fuzzy-search it and hit enter to connect. Most-used hosts float to the top, you can tag them, and jump
hosts are just a field instead of something you retype.
None of that is the interesting part though. This post is about the two design decisions people keep asking about: why it doesn't build on ~/.ssh/config, and how it fills in passwords for password-auth hosts without sshpass.
Why not just use ~/.ssh/config
Fair question, I get it a lot. A few reasons it keeps its own host db instead.
That file is shared state. Ansible reads it, my editor reads it, other tools read it, and a couple of them like to rewrite it. I've had something reformat mine and drop my comments, and once was enough. I didn't want to be one more writer fighting over that file.
It also just can't do some of what I wanted. There's nowhere to put a password. There's no notion of "hosts I actually use", so no most-recently-used ordering. Tags aren't really a thing. So instead of bending ssh config into shapes it doesn't want, sshelf keeps its own db and builds the ssh command from it:
ssh -i ~/.ssh/infra-key -p 2222 -J bastion -o StrictHostKeyChecking=accept-new mike@10.25.25.25
Plain flags, no temp -F config file, nothing written back to your config. If you've already got hosts in ssh config there's an import, but it only ever reads, never writes.
One more bit while I'm here: on connect it doesn't run ssh as a child process and sit on top of it. It tears the TUI down and exec()s into ssh, so ssh replaces the sshelf process entirely. When you log out you're back at your shell with nothing wrapping the session. The
catch with exec is that it never returns, so anything you care about (like bumping the usage stats for that host) has to happen before the call, and you need a guard plus a panic hook so the terminal gets put back on every other way out.
The actual password problem
Most of my hosts use keys, and you should too. But the real world has IPMI cards, old appliances, and vendor boxes where password auth is all you get. Your options there are both bad:
- Retype the password every time, copy-pasting from a password manager if you're tidy.
-
sshpass -p hunter2 ssh ..., which sticks the password in argv where anyone on the box can read it out ofps, and tends to land in your shell history too.
What I wanted: the password sits in the OS keyring (Keychain on mac, Secret Service on Linux), or an age-encrypted vault on headless machines, and ssh just gets it when it asks, without the secret ever touching a command line.
SSH_ASKPASS, and the part nobody mentions
ssh has had SSH_ASKPASS forever. Point it at a program and, when ssh needs a passphrase and there's no terminal to prompt on, it runs that program and reads the answer off stdout. The problem was always that "no terminal" bit. In a normal interactive session ssh ignored it.
OpenSSH 8.4 added the missing piece, SSH_ASKPASS_REQUIRE=force, which tells ssh to use the askpass program even when there is a TTY. So the flow ends up like this:
- You hit enter on a password host.
- sshelf sets
SSH_ASKPASSto its own path,SSH_ASKPASS_REQUIRE=force, and a couple of its own env vars (one flag that says "you're running as the askpass helper now", and the id of the host being connected to). - It execs into ssh.
- ssh wants the password, so it runs the helper, which is just the sshelf binary again. The helper sees the flag, looks up the secret for that host id in the keyring or vault, prints it, and exits 0.
No second binary to ship, no secret in argv, nothing in ps. The same path handles encrypted key passphrases too, since a host only has one stored secret either way.
The footgun: "force" really does mean everything
Here's the part that ate an evening. With SSH_ASKPASS_REQUIRE=force, ssh sends every prompt through your helper. Including this one:
The authenticity of host '...' can't be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
A naive helper that always prints the password answers that host-key question with hunter2, ssh asks again, and congratulations, you've built an infinite loop. I hit exactly this the first time I tested against a real password sshd in a container.
It gets worse. With keyboard-interactive auth the server controls the prompt text. So a shady server could send you a prompt that just happens to contain the word "password" and scoop up whatever the helper prints.
So the helper actually looks at the prompt it was handed (it comes in as argv[1]) and only answers things shaped like a real OpenSSH secret prompt:
- ends with
password:(the classicuser@host's password:, or PAM'sPassword:) - contains
passphrase for(Enter passphrase for key '...':)
Anything else, it exits non-zero and lets ssh deal with it the normal way. So "Type your password to continue:" from some custom server gets declined, because it doesn't match the shape. On top of that sshelf passes StrictHostKeyChecking=accept-new, so the host-key prompt usually doesn't even fire for new hosts (a changed key on a host you already know still hard-fails, which is the protection you actually want), and each secret is scoped to a single host so a mis-answer can't leak the wrong one.
Limitations, because this is a security post
- Password-auth jump hosts don't work yet. The helper only has the target's secret and can't tell which hop is asking, so jump hosts have to use keys or an agent for now.
- The password autofill needs OpenSSH 8.4 or newer at runtime.
- mac and Linux only. The clean exit-to-shell leans on Unix
exec(), and there's no real Windows equivalent. - And the obvious one: keys and an agent beat stored passwords every time. This is for the boxes that don't give you the choice, not a recommendation to start saving passwords.
Wrapping up
sshelf is Rust and ratatui, MIT/Apache, with prebuilt binaries for mac
and Linux:
brew install max-rh/tap/sshelf
or grab the installer or a .deb from the releases page.
It's still early (v0.2.0), so if you try it and it breaks on your setup, an issue would make my week. Repo's here: https://github.com/max-rh/sshelf













