Skip to content
DevOps2026-09-087 min read

Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)

Symptom: git refuses to talk to the remote

git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

One-line diagnosis: SSH authentication failed — the server doesn't recognize the public key you presented. This has nothing to do with passwords (publickey auth doesn't use one). Either the key in your hand doesn't match any lock registered on the server, or you never handed over a key at all.

First step: test auth directly with ssh -T

Skip git and test the SSH channel itself — you'll get the cleanest error:

ssh -T git@github.com
# Success: Hi yourname! You've successfully authenticated...
# Failure: git@github.com: Permission denied (publickey).

# Add -v for the full story: which keys were offered, which auth the server accepts
ssh -vT git@github.com 2>&1 | grep -E 'Offering|identity file|denied'

The Offering public key: /Users/you/.ssh/id_xxx lines in the -v output are the key evidence — they list the keys actually presented. If the one you thought you were using isn't there, jump straight to cause 2.

Six causes, ranked by frequency

1. No key on this machine (most common on a new laptop)

ls ~/.ssh/
# id_ed25519 / id_ed25519.pub or id_rsa / id_rsa.pub → you have one
# nothing → generate one
ssh-keygen -t ed25519 -C "your_email@example.com"
# accept the defaults (or set a passphrase), then start the agent and load it
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

Then add the contents of the .pub file (public keys only — the private key never leaves your machine) to GitHub: Settings → SSH and GPG keys → New SSH key.

One permission note: ~/.ssh must be 700 and the private key 600 — OpenSSH refuses keys readable by group/others ("unprotected private key file"). If the numbers escape you, the chmod calculator turns checkboxes into the exact command.

2. Key exists, but the agent isn't offering it

ssh-add -l
# "The agent has no identities." → not loaded
ssh-add ~/.ssh/id_ed25519

On macOS, put the key metadata in ~/.ssh/config and skip the manual add forever:

Host github.com
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519

3. Public key never added to the platform (or added to the wrong account)

GitHub keys are account-scoped: a key registered under account A gets rejected for account B's repos. Another trap: the same public key is already bound to a different GitHub account — GitHub doesn't allow one key on multiple accounts, and the same applies to deploy keys. For multi-account setups, see cause 4.

Company GitLab, Gitee, and self-hosted servers each have their own key-upload page — make sure you added it to the platform you're actually pushing to.

4. Multiple accounts/keys: the wrong key gets offered

With a work key and a personal key on the same machine, SSH tries them in order and often presents the wrong one. Route by host alias in ~/.ssh/config:

# Personal GitHub
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_personal
  IdentitiesOnly yes

# Work GitHub account via an alias
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

IdentitiesOnly yes is the critical line — it forces SSH to offer only the specified key instead of trying every key it can find. Then use the alias in your remote URL:

git remote set-url origin git@github-work:company/repo.git

5. Deploy keys have a permission boundary

A public key added under a repo's Settings → Deploy keys is valid for that repo only: cloning a different repo with it, or pushing when the key was added without write access, both end in publickey rejection. The classic CI trap: one runner using one deploy key to pull many repos — use organization-level deploy keys or a machine-user account instead.

6. Wrong remote URL (https and ssh mixed up)

git remote -v
# origin  git@github.com:user/repo.git     → SSH; debug with the methods above
# origin  https://github.com/user/repo.git → HTTPS; errors ask for a password/token, not publickey

Case mismatches, misspelled org names, and a missing .git suffix all blur the line between "repo not found" and "auth failed". Also note: if a GitHub organization enforces SSO, a personal key still needs a one-time authorization for that org (SSO → Authorize).

Why does it say publickey when I never configured keys?

The word in parentheses is the list of auth methods the server permits. If you see Permission denied (publickey,password), both methods were tried and both failed — SSH attempts each method the server allows before giving up. Read the parentheses to see which doors exist.

Windows OpenSSH: same protocol, different agent path

Git for Windows ships its own OpenSSH; Windows 10/11 also ship a system OpenSSH. Mixing them is a classic publickey ghost: you ssh-add into one agent while git invokes the other binary. Check:

where.exe ssh
Get-Service ssh-agent   # start + set to Automatic if needed
ssh-add -l

Point Git at one stack (git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe") and load keys into that agent. Pageant/PuTTY keys are not visible to OpenSSH unless you bridge them — prefer a single agent story.

Host key changed vs publickey denied

REMOTE HOST IDENTIFICATION HAS CHANGED is not publickey failure — the server's host key no longer matches ~/.ssh/known_hosts. After a rebuild or IP reuse, remove the stale line (ssh-keygen -R hostname) only when you trust the new host fingerprint. Confusing the two wastes hours: fixing your user key never clears a host-key mismatch.

Agent forwarding: convenience with a blast radius

ssh -A / ForwardAgent yes lets a jump host use your local key for the next hop. Useful for bastions; dangerous when the bastion is shared — a compromised hop can request signatures from your agent. Prefer ProxyJump with a dedicated deploy key on the bastion, or short-lived certificates. If -v shows offerings you did not expect on the second hop, check whether forwarding is on.

Key type expectations (ed25519 first)

Most hosts accept ssh-ed25519 today; some older appliances still want rsa with ≥3072 bits. If -v shows the client offering ed25519 and the server closing without trying RSA, generate an RSA key for that host only and pin it with IdentityFile + IdentitiesOnly yes. Do not weaken global defaults for one legacy box. Hardware-backed keys (ed25519-sk) behave like normal keys once enrolled — failure modes are the same (not loaded / not registered), plus a missing FIDO touch.

Checklist

  1. ssh -T git@github.com — direct test, cleanest error message
  2. ssh -vT ... | grep Offering — which keys were actually presented?
  3. ls ~/.ssh/ + ssh-add -l — does the key exist? is it loaded?
  4. Is the public key registered on the right platform, account (or repo deploy key)?
  5. Multiple keys: ~/.ssh/config with per-host aliases + IdentitiesOnly yes
  6. git remote -v — check the URL spelling and protocol (ssh vs https)
  7. Windows: one OpenSSH binary + matching agent; rule out known_hosts host-key errors before chasing user keys

Triage notes

The article's first step does not investigate git — it goes around it: ssh -T tests the auth channel directly and gets you the cleanest error message. The 'Offering' lines in -v output are the diagnostic core, listing the keys actually handed over; if the key you believe you are using is not on that list, jump straight to the multiple-keys entry. IdentitiesOnly yes is marked critical in multi-account setups precisely so SSH does not offer every key in turn. Host-key changes get their own clarification that they are not publickey failures — changing user keys will never clear a known_hosts conflict, and confusing the two wastes a whole debugging session.


Provided by ToolVault. Related tools: Linux Cheatsheet, Password Generator, JWT Decoder, chmod calculator. Related reading: npm registry mirror guide, Port already in use. See the homepage for more developer tools.