An SSH key that won’t authenticate to GitHub or GitLab is one of the most-Googled developer problems there is, and half the time the key is fine — the diagnosis is just wrong. So before you regenerate anything, run one test and learn to read its answer. Then work the culprits in order of how often they’re actually the cause.
Step 0: the test that tells you if it’s even broken
Ask the git host directly whether it recognizes your key:
ssh -T [email protected] # GitHub
ssh -T [email protected] # GitLab
Now read the response carefully, because the success message looks like an error:
Hi <username>! You've successfully authenticated, but GitHub does not
provide shell access.
That is success. “Does not provide shell access” is not a failure — GitHub and GitLab don’t give you a shell, they only speak git over SSH, so they authenticate you and then hang up. People see “does not provide” and start troubleshooting a working setup. If you see your username in that line, your key works; your problem is somewhere else (jump to the HTTPS remote culprit below — that’s the usual real issue).
The genuine failure looks like this instead:
[email protected]: Permission denied (publickey).
That’s the one we’re here to fix. Note the endpoint: Permission denied (publickey) means the server accepted no key you offered. Work down the list.
Culprit 1: the key isn’t added to your account
The most common real cause. A key on your laptop does nothing until its public half is registered with the git host. Copy the public key:
cat ~/.ssh/id_ed25519.pub # note the .pub — this is the safe half to share
Paste that into GitHub → Settings → SSH and GPG keys → New SSH key, or GitLab → Preferences → SSH Keys.
One rule, no exceptions: paste the .pub file, never the file without .pub. The public key is one line starting ssh-ed25519 AAAA… (or ssh-rsa AAAA…). If what you’re about to paste starts with -----BEGIN OPENSSH PRIVATE KEY-----, stop — that’s your private key, the secret that is your identity. Pasting it into a web form hands your credential to anyone who can read that page. Share the public half; guard the private half.
(No key yet? Generate one — Ed25519 in almost all cases: ssh-keygen -t ed25519 -C "you@device".)
Culprit 2: the wrong key is being offered
Your key is registered, but SSH is offering a different one — common when you carry several keys and the agent tries them in an order the server rejects. See exactly which key is sent:
ssh -vT [email protected]
Look for the Offering public key: lines. If the key you registered isn’t among them, or a wrong one is being accepted, pin the right key per host in ~/.ssh/config:
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
IdentitiesOnly yes is the key line — it tells SSH to offer only the IdentityFile you named and stop spraying every key in your agent at the server (which also cures Too many authentication failures). Do the same with Host gitlab.com for GitLab. More on structuring the file in the ssh_config guide.
Culprit 3: the agent isn’t running, or the key isn’t loaded
If your key has a passphrase and the agent doesn’t hold it, SSH may skip it silently. Check what the agent has:
ssh-add -l
The agent has no identities (or Could not open a connection to your authentication agent) means you need to start it and load the key:
eval "$(ssh-agent -s)" # start the agent in this shell
ssh-add ~/.ssh/id_ed25519 # load the key (prompts for the passphrase once)
ssh-add -l should now list your key’s fingerprint. (For what the agent actually is and how it holds keys, see what is the SSH agent.)
Culprit 4: you’re on an HTTPS remote, not an SSH one
This is the culprit behind “my key works (ssh -T succeeded) but git push still asks for a password.” Your repository is configured to use HTTPS, which ignores SSH keys entirely and wants a token. Check:
git remote -v
If you see https://github.com/you/repo.git, that’s the problem — HTTPS never touches your SSH key. Switch the remote to SSH:
git remote set-url origin [email protected]:you/repo.git # GitHub
git remote set-url origin [email protected]:you/repo.git # GitLab
Run git remote -v again to confirm it now starts with git@. This single fix resolves a huge share of “SSH key not working” reports where the key was fine all along.
Culprit 5: permissions on ~/.ssh
SSH refuses to use keys and config that others could tamper with. If verbose output mentions bad modes, or keys are being ignored for no clear reason:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 # private key
chmod 644 ~/.ssh/id_ed25519.pub # public key
chmod 600 ~/.ssh/config
On Windows this surfaces after copying keys off a synced drive or between machines; fix it via the file’s Security properties, or keep keys out of cloud-synced folders.
Culprit 6: passphrase prompts every time
If every git operation demands your key passphrase, the agent isn’t caching it. Load it once with ssh-add (culprit 3), and make it stick across sessions by adding to ~/.ssh/config:
Host github.com gitlab.com
AddKeysToAgent yes
On macOS, ssh-add --apple-use-keychain ~/.ssh/id_ed25519 stores the passphrase in Keychain so it survives reboots.
Culprit 7: two accounts, one host
Work and personal GitHub accounts can’t share one key — a given public key belongs to exactly one account. Give each its own key and a Host alias that selects it:
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/work_ed25519
IdentitiesOnly yes
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/personal_ed25519
IdentitiesOnly yes
Then clone or set the remote with the alias as the host: git@github-work:org/repo.git. SSH matches github-work, picks the work key, and rewrites the connection to github.com under the hood.
Culprit 8: organization SSO not authorized (GitHub)
You authenticate fine to your own repos but get Permission denied on an org’s repos behind SAML SSO. The key exists but isn’t authorized for that organization. Go to Settings → SSH and GPG keys, find the key, click Configure SSO, and authorize it for the organization. GitLab has a parallel case where an expired key is silently rejected — check the key’s expiry date in SSH Keys if a previously working key suddenly stops.
When it’s still Permission denied (publickey)
If you’ve worked the list and still hit the endpoint, you’ve narrowed it to a genuine key-acceptance problem — the systematic loop for that exact error (reading ssh -vvv, the server’s method list, offered vs. accepted keys) is in permission denied (publickey), a fast systematic fix, and the broader map of SSH errors is in SSH errors decoded. But for git hosts specifically, the order above catches nearly everything: register the public key, offer the right one, load it into the agent, and make sure you’re actually on an SSH remote.
Where a client removes the setup entirely
Most of this friction is bookkeeping — which key belongs to which host, whether it’s loaded, whether the remote is even SSH. Termalin keeps the key with the host it belongs to, unlocks your keys once through a built-in agent so passphrases aren’t retyped (and a cancelled prompt never silently skips a key), and — because it speaks the standard ssh-agent protocol — git and your CLI can sign through that same agent. Fingerprints are visible per key, so registering the right public half with GitHub or GitLab is copy-and-paste rather than a hunt through five ~/.ssh folders. The account-and-alias juggling above becomes a per-host setting instead of a config-file puzzle.
Termalin is a free, cross-platform SSH client that manages your keys with a one-unlock agent git can sign through — download it, or read how it handles keys safely.