Skip to content
Dev Tools Article

A Missing .pub File Can Lock You Out of GitHub

OpenSSH silently switches authentication flows based on a file that plays no cryptographic role.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Jul 21, 2026 · 4 min read
A Missing .pub File Can Lock You Out of GitHub

Here's a failure mode worth filing away before it bites you: a developer's GitHub pulls suddenly died with Permission denied (publickey) — key still registered on GitHub, nothing changed locally, second laptop working fine — and the fix was regenerating a file that plays no cryptographic role in authentication at all. The private key was intact. What was missing was the .pub file sitting next to it.

That sounds absurd until you know what that file actually does to OpenSSH's client behavior. It's not a convenience copy. It's a switch that selects between two different authentication flows — and the evidence suggests GitHub's SSH edge stopped accepting one of them.

One protocol, two flows

SSH public-key auth, per RFC 4252, gives clients a choice. The polite flow: send an unsigned probe ("would you accept this key?"), wait for the server's PK_OK, then sign and authenticate. The impatient flow: skip the probe and send a fully signed authentication request in one shot. Both are legal. Stock sshd accepts both.

The OpenSSH client picks between them based on what it has on hand. If it knows the public key — because a .pub file sits next to the private key, or an agent is serving the key — it probes first. You've seen this in ssh -v output as Offering public key:. The probe exists for good reasons: it avoids prompting you for a passphrase, or making your YubiKey blink, for a key the server was going to reject anyway.

But if all OpenSSH has is a path to a private key with no .pub beside it, probing buys nothing — it has to load the private key either way to learn the public half. So it loads the key and signs immediately. That's the Trying private key: path in the debug output. Same key, same signature algorithm, materially different wire behavior — determined by the presence of a file most developers couldn't tell you the purpose of.

The blogger who hit this, Erik Thorsell, did the legwork you'd want: twelve alternating trials, and the result was deterministic. No .pub: rejected. .pub present: accepted. That's not flaky networking; that's the server treating the two flows differently.

What GitHub probably changed — and what we actually know

Here's where the certainty runs out, and it's worth being precise about it. Thorsell's hypothesis is that GitHub's SSH frontend was updated to reject direct-signed first attempts. That's consistent with his data, but nobody outside GitHub has confirmed it, there's nothing in GitHub's changelog, and as of this writing there's no wave of matching reports. The new server banner he observed proves little on its own — GitHub's SSH edge has long identified itself as babeld-<build-hash>, a custom frontend built for GitHub's scale, and that hash changes with routine deploys.

But the shape of the story is entirely plausible, because GitHub has form here. It dropped SHA-1-signed RSA authentication in March 2022, removed DSA key support the same month, and rotated its RSA host key in 2023 after an exposure — each time breaking some long-tail population of clients that was technically doing something legal. And a custom SSH implementation is exactly where you'd expect the two RFC-legal flows to diverge in handling: the direct-signed path is the rare one, exercised mostly by clients missing their .pub files, so a regression or a deliberate tightening there could sit in production for days before anyone connected the dots. If it is deliberate — a defense against auth-request spraying, say — GitHub should say so, because right now the failure is indistinguishable from user error.

The population that should worry: CI

For a laptop, the fix is one command:

ssh-keygen -y -f ~/.ssh/github_key > ~/.ssh/github_key.pub

(You'll need the passphrase if the key's encrypted; check permissions on the result.) Diagnosis is one flag: run ssh -vT git@github.com and look for Trying private key where you'd expect Offering public key. If you see the former, you're on the direct-sign path.

The population that should actually care, though, isn't laptop users — it's CI and automation. The standard pattern for deploy keys is to stash the private key in a secrets manager and write it out in the job:

echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

No .pub file. Every pipeline built this way authenticates via the direct-sign flow. If GitHub is genuinely rejecting that flow — even intermittently, even on a partial rollout — this pattern is a time bomb, and the blast radius is every container image and pipeline template that bakes in a bare private key. The hardening is cheap: add ssh-keygen -y to your setup step, or better, load the key into ssh-agent in the job, since agent-served keys always take the probe path. Do it now, while it's a one-line PR instead of a 3 a.m. incident.

GitHub's edge is the de facto spec

The durable lesson isn't the fix. It's that for SSH-based Git, the RFC stopped being the spec a long time ago — GitHub's implementation is. When one endpoint terminates the overwhelming majority of the world's Git-over-SSH connections, whichever subset of the protocol it accepts is the protocol, the same way "works in Chrome" quietly became the definition of the web. Every SSH client author now has a de facto conformance target that ships unannounced, on someone else's deploy schedule.

So, two takeaways with different confidence levels. High confidence: the .pub file changes your client's wire behavior, and you should treat it as part of the key material — store it, deploy it, keep it next to the private key everywhere, including in CI. Lower confidence, pending GitHub saying anything at all: the edge got stricter about a flow the RFC explicitly permits. If your pipelines started throwing Permission denied (publickey) this week with keys you haven't touched, you now know the first thing to check — and it isn't your key.

Sources & further reading

  1. GitHub suddenly rejected my SSH key (the fix was a .pub file?!) — thorsell.io
  2. RFC 4252: The Secure Shell (SSH) Authentication Protocol — rfc-editor.org
  3. We updated our RSA SSH host key — github.blog
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 1

Join the discussion

Sign in or create an account to comment and vote.

Zhilakai @zhilakai · 43 minutes ago

Not sure I buy that OpenSSH is 'silently' switching flows here though. The tool's doing exactly what you'd expect if a pubkey file is missing—it just tries different auth methods. The real issue is that GitHub deprecated something without enough of a grace period, right? Blaming OpenSSH for being quiet feels like misdirecting when the actual problem is the platform.

Related Reading