DEV Community

Max RH
Max RH

Posted on

Auto-supplying SSH passwords without sshpass: the SSH_ASKPASS trick

I built sshelf, a terminal UI for managing SSH hosts.
Save each host once (key, port, jump hosts, tags), then fuzzy-search and hit Enter to connect. This post is about the two design decisions people ask about most: why it never touches ~/.ssh/config, and how it auto-fills passwords without sshpass.

Why I didn't build on ~/.ssh/config

Every SSH manager I tried was a frontend over ~/.ssh/config: it either read the file or, worse, rewrote it. That file is shared state. Ansible reads it. IDEs read it. Some tools edit it in place. I've had a tool mangle mine once, and once was enough.

There's also a capability gap. ssh config can't store passwords at all, has no notion of "hosts I use often", and tags don't exist. So sshelf keeps its own TOML database and generates the ssh invocation from it:

ssh -i ~/.ssh/infra-key -p 2222 -J bastion -o StrictHostKeyChecking=accept-new mike@10.25.25.25
Enter fullscreen mode Exit fullscreen mode

Plain flags, no temporary -F config files, so "never touches your ssh config" stays literally true. Getting started doesn't require retyping everything: there's an import from ~/.ssh/config, but it's strictly read-only.

One more piece: on connect, sshelf doesn't spawn ssh as a child and babysit it. It tears down the TUI and exec()s into ssh, replacing its own process. When the session ends you land back at your shell, not inside a wrapper. Two consequences of exec() never
returning: usage stats must be persisted before the call, and the terminal teardown needs a RAII guard plus a panic hook so every other exit path restores your terminal too.

The password problem

Most of my hosts use keys. But the real world has appliances, IPMI interfaces, and vendor boxes where password auth is what you get. The usual options are bad:

  • Retype the password every time (from a password manager, if you're disciplined).
  • sshpass -p hunter2 ssh …, which puts the secret in argv, where any user on the machine can read it out of ps. It also tends to end up in shell history.

What I wanted: password lives in the OS keyring (macOS Keychain / Linux Secret Service), or in an age-encrypted vault on headless machines, and ssh
gets it automatically, without the secret ever touching a command line.

Enter SSH_ASKPASS

OpenSSH has had SSH_ASKPASS forever: point it at a program, and when ssh needs a passphrase without a terminal, it runs that program and reads the secret from its stdout. The catch was "without a terminal" — in an interactive session, ssh ignored it.

OpenSSH 8.4 (2020) added the missing piece: SSH_ASKPASS_REQUIRE=force makes ssh use the askpass program even when a TTY is present. So the flow becomes:

  1. You hit Enter on a password-auth host.
  2. sshelf sets SSH_ASKPASS=<path to itself>, SSH_ASKPASS_REQUIRE=force, plus two of its own env vars: a flag marking askpass mode and the host id.
  3. It exec()s into ssh.
  4. ssh needs the password, so it runs the helper — which is the same sshelf binary, re-exec'd. The helper sees the mode flag, fetches the secret for that host id from the keyring (or vault), prints it, exits 0.

No second binary to install, no secret in argv or env, nothing in ps. The same mechanism answers key passphrase prompts for encrypted keys, since a host's one stored secret is unambiguous.

The footgun: force means everything

Here's the part that cost me an evening. With SSH_ASKPASS_REQUIRE=force, ssh routes every interactive prompt through the 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 the host-key question with hunter2, ssh re-asks, and you've built an infinite loop. I hit exactly this testing against a containerized sshd.

Worse, prompts aren't always from OpenSSH itself. With keyboard-interactive auth, the server controls the prompt text. A malicious or compromised server could send a prompt that merely mentions "password" and harvest whatever your helper prints.

So the helper inspects the prompt it's handed (it arrives as argv[1]) and matches the shape of OpenSSH's real prompts, not substrings:

  • ends with password: (the classic user@host's password:, PAM's Password:) → answer
  • contains passphrase for (Enter passphrase for key '…':) → answer
  • anything else → exit non-zero, which tells ssh to handle it some other way

"Type your password to continue:" fails the shape test and gets declined. Two more layers on top: sshelf passes -o StrictHostKeyChecking=accept-new so the host-key prompt rarely
fires in the first place (known-host key changes still hard-fail, so MITM protection for known hosts is intact), and each secret is scoped to one host, which caps the damage if something ever is mis-answered.

Limitations, since every security post needs them

  • Password-auth jump hosts don't work in v1: the helper only holds the target's secret and can't tell which hop is prompting. Jump hosts need key/agent auth for now.
  • Password auto-supply needs OpenSSH 8.4+ at runtime.
  • macOS + Linux only. The exec handoff is Unix exec(); there's no Windows equivalent.
  • And the obvious one: keys and agents are still better than stored passwords. sshelf's docs say so too. This feature is for the hosts where you don't get a choice.

Wrapping up

sshelf is Rust + ratatui, dual-licensed MIT/Apache, with prebuilt
binaries for macOS and Linux:

brew install max-rh/tap/sshelf
Enter fullscreen mode Exit fullscreen mode

or the shell installer / .debs on the releases page.
It's v0.2.0, so early days. If you try it and something breaks, an issue would make my day:
https://github.com/max-rh/sshelf

Top comments (0)