<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max RH</title>
    <description>The latest articles on DEV Community by Max RH (@max-rh).</description>
    <link>https://dev.to/max-rh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3974310%2Fb12d37de-d04c-45b9-b5ea-634c59966e98.png</url>
      <title>DEV Community: Max RH</title>
      <link>https://dev.to/max-rh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/max-rh"/>
    <language>en</language>
    <item>
      <title>Auto-supplying SSH passwords without sshpass: the SSH_ASKPASS trick</title>
      <dc:creator>Max RH</dc:creator>
      <pubDate>Tue, 09 Jun 2026 12:29:47 +0000</pubDate>
      <link>https://dev.to/max-rh/auto-supplying-ssh-passwords-without-sshpass-the-sshaskpass-trick-49ig</link>
      <guid>https://dev.to/max-rh/auto-supplying-ssh-passwords-without-sshpass-the-sshaskpass-trick-49ig</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/max-rh/sshelf" rel="noopener noreferrer"&gt;sshelf&lt;/a&gt;, a terminal UI for managing SSH hosts.&lt;br&gt;
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 &lt;code&gt;~/.ssh/config&lt;/code&gt;, and how it auto-fills passwords without &lt;code&gt;sshpass&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't build on ~/.ssh/config
&lt;/h2&gt;

&lt;p&gt;Every SSH manager I tried was a frontend over &lt;code&gt;~/.ssh/config&lt;/code&gt;: 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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;generates&lt;/em&gt; the ssh invocation from it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -i ~/.ssh/infra-key -p 2222 -J bastion -o StrictHostKeyChecking=accept-new mike@10.25.25.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

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

&lt;h2&gt;
  
  
  The password problem
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;What I wanted: password lives in the OS keyring (macOS Keychain / Linux Secret Service), or in an &lt;a href="https://age-encryption.org/" rel="noopener noreferrer"&gt;age&lt;/a&gt;-encrypted vault on headless machines, and ssh&lt;br&gt;
gets it automatically, without the secret ever touching a command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter SSH_ASKPASS
&lt;/h2&gt;

&lt;p&gt;OpenSSH has had &lt;code&gt;SSH_ASKPASS&lt;/code&gt; 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.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;You hit Enter on a password-auth host.&lt;/li&gt;
&lt;li&gt;sshelf sets &lt;code&gt;SSH_ASKPASS=&amp;lt;path to itself&amp;gt;&lt;/code&gt;, &lt;code&gt;SSH_ASKPASS_REQUIRE=force&lt;/code&gt;, plus two of
its own env vars: a flag marking askpass mode and the host id.&lt;/li&gt;
&lt;li&gt;It &lt;code&gt;exec()&lt;/code&gt;s into ssh.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  The footgun: force means &lt;em&gt;everything&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the part that cost me an evening. With &lt;code&gt;SSH_ASKPASS_REQUIRE=force&lt;/code&gt;, ssh routes &lt;em&gt;every&lt;/em&gt; interactive prompt through the helper. Including this one:&lt;br&gt;
    The authenticity of host '…' can't be established.&lt;br&gt;
    Are you sure you want to continue connecting (yes/no/[fingerprint])?&lt;/p&gt;

&lt;p&gt;A naive helper that always prints the password answers the host-key question with &lt;code&gt;hunter2&lt;/code&gt;, ssh re-asks, and you've built an infinite loop. I hit exactly this testing against a containerized sshd.&lt;/p&gt;

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

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

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

&lt;p&gt;"Type your password to continue:" fails the shape test and gets declined. Two more layers on top: sshelf passes &lt;code&gt;-o StrictHostKeyChecking=accept-new&lt;/code&gt; so the host-key prompt rarely&lt;br&gt;
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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, since every security post needs them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Password-auth &lt;em&gt;jump&lt;/em&gt; 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.&lt;/li&gt;
&lt;li&gt;Password auto-supply needs OpenSSH 8.4+ at runtime.&lt;/li&gt;
&lt;li&gt;macOS + Linux only. The exec handoff is Unix &lt;code&gt;exec()&lt;/code&gt;; there's no Windows equivalent.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;sshelf is Rust + &lt;a href="https://ratatui.rs" rel="noopener noreferrer"&gt;ratatui&lt;/a&gt;, dual-licensed MIT/Apache, with prebuilt&lt;br&gt;
binaries for macOS and Linux:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install max-rh/tap/sshelf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;or the shell installer / &lt;code&gt;.deb&lt;/code&gt;s on the &lt;a href="https://github.com/max-rh/sshelf/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;.&lt;br&gt;
It's v0.2.0, so early days. If you try it and something breaks, an issue would make my day:&lt;br&gt;
&lt;a href="https://github.com/max-rh/sshelf" rel="noopener noreferrer"&gt;https://github.com/max-rh/sshelf&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ssh</category>
      <category>security</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
