DEV Community

Cover image for Everyone Types "yes"
Den
Den

Posted on

Everyone Types "yes"

The smoke test for my little SSH key program is one command: clone a repository of mine over SSH. On a fresh machine, before anything is cloned, it asks this:

The authenticity of host 'github.com (<IP address>)' can't be established.
ED25519 key fingerprint is SHA256:<key fingerprint>.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Enter fullscreen mode Exit fullscreen mode

And I typed yes. Like everyone does.

This series has been about two things so far: keeping the passphrase out of ps, and checking whether a tool uses your SSH at all. The second post leaned on one defence — the wall of @@@ OpenSSH throws when a server's key changes. This post is about the uncomfortable part: that wall protects you only if the key it's comparing against was the right one. And that key got there when you typed yes.

Trust on first use

SSH's model has a name: TOFU, trust on first use. The first time you connect, the client has nothing to compare against, so it asks you. Whatever you accept goes into known_hosts, and from then on any change is an alarm.

It's a reasonable design. The weak point is obvious: if someone is between you and the server on that first connection, you accept their key, and from then on the alarm protects the impostor. Everything after the first yes is only as good as the first yes.

Most of the time this is theoretical. Then sometimes it isn't.

March 2023

On March 24, 2023, GitHub replaced its RSA SSH host key. The private half had been briefly exposed in a public repository; anyone who copied it could, in principle, pretend to be github.com to RSA clients.

So every RSA user got the wall: REMOTE HOST IDENTIFICATION HAS CHANGED. Exactly the alarm that's supposed to make you stop and think.

GitHub published the new fingerprint and instructions. But the fix that spread fastest was the short one: ssh-keygen -R github.com, connect again, type yes. Which works — and quietly accepts whatever key answered that next connection, without comparing it to anything. The alarm did its job; the habit undid it.

What "verifying" actually means

The question at the prompt isn't "do you want to connect". It's "does this fingerprint match the one you got some other way". The point is some other way — a channel an attacker on your network path can't also fake.

For the big hosts that channel exists. GitHub, GitLab and Bitbucket publish their host key fingerprints in their documentation, served over HTTPS. Don't take them from this post, by the way — a fingerprint in a blog post is exactly as trustworthy as the blog. Open the provider's docs page.

Then there's a feature I'd used for years without noticing: you can paste the fingerprint instead of typing yes. Note the [fingerprint] in the prompt. Copy the published value, paste it, and OpenSSH compares it for you. A mismatch is refused. No squinting at forty base64 characters.

Already have hosts in known_hosts and want to check them after the fact?

ssh-keygen -l -F github.com
Enter fullscreen mode Exit fullscreen mode

prints the fingerprints of what you stored. Compare with the docs.

ssh-keyscan is not verification

In the previous post I used ssh-keyscan to plant a deliberately wrong key — that's what it's good for. It's also widely used for something it can't do: "trusting" a host in scripts and CI.

ssh-keyscan github.com >> ~/.ssh/known_hosts
Enter fullscreen mode Exit fullscreen mode

This downloads the key from whoever answered on port 22. If that's an impostor, you've just written the impostor's key down as trusted, automatically, with no prompt. It's the scripted version of typing yes without reading.

GitHub's own recovery instructions in 2023 did something better: they fetched the keys from the API over HTTPS —

curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | sed -e 's/^/github.com /' >> ~/.ssh/known_hosts
Enter fullscreen mode Exit fullscreen mode

— which moves the trust to TLS, a channel you already rely on. Better still for CI: copy the published known_hosts lines once, check them into the repository, and use that file. A pinned key changes only when you change it.

Two settings worth knowing while you're there:

  • StrictHostKeyChecking accept-new — accept unknown hosts automatically, but still refuse changed keys. A sane middle ground for automation.
  • StrictHostKeyChecking no — accept everything, including changed keys. If you find it in a pipeline, that pipeline has no host verification at all.

Closing the series

Three posts, three places the same thing goes wrong — a secret or a trust decision handled in a way that looks fine and isn't:

  1. The passphrase goes through ssh-agent or SSH_ASKPASS, never through argv.
  2. The SSH client is the one you configured — check that your tools read your config, check host keys, and use your agent.
  3. The first host key is compared against a fingerprint from another channel, not accepted with yes or fetched with ssh-keyscan.

None of this needs new software. Everything in this series is already in OpenSSH — the askpass hook, the config, the fingerprint prompt, accept-new. The defaults are good. The weak spot is almost never the protocol; it's the shortcut around it.

As for my smoke test: it now starts by pasting the fingerprint.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.