You run git push, expect a normal deploy or branch update, and Git answers with the least helpful message in version control: Permission denied (publickey).

The good news is that this error is usually smaller than it looks. It feels like GitHub, GitLab, or Bitbucket is rejecting you. In practice, the problem is usually that your machine didn't present a valid SSH identity during the handshake, so the server never got far enough to decide whether you should have repo access.

That distinction matters. If you're troubleshooting permission denied publickey in Git, you're usually fixing local authentication, not project authorization. This is the same mindset behind protecting your business with secure authentication. Strong systems fail closed when identity isn't proven.

If you're interested in broader engineering workflow topics around debugging and shipping, the Appjet blog is also worth browsing after you get this unblocked.

Understanding the Permission Denied Error in Git

You run git push, watch SSH connect, and then get Permission denied (publickey). At that point Git has not reached your branch rules, repo role, or merge policy. The SSH handshake failed first, which means the server did not accept any identity your machine offered.

That distinction saves time. Treat this as an authentication problem on the client side until the evidence says otherwise. Teams that already care about protecting your business with secure authentication usually recognize the pattern quickly. The system is failing closed because your identity was not proven.

The fastest way to read the failure is verbose SSH output:

ssh -vT git@host

That command shows the diagnostic trail you need. Which keys SSH tried, whether an agent was involved, whether a config file changed the identity, and where the server said no. This guide follows that same order, from common mistakes to the awkward edge cases that waste an afternoon. If you like practical debugging writeups in the same style, the Appjet engineering blog is worth keeping in your reading list.

What this error usually points to

In practice, Permission denied (publickey) usually means one of five things:

  • No usable SSH key exists on the machine you're using.
  • The matching public key was never added to the Git host account you expect to use.
  • The SSH agent did not load the key, so SSH never offered it.
  • File permissions on ~/.ssh or the key files are loose enough that SSH refuses to trust them.
  • SSH connected with the wrong identity or the wrong host alias, which is common with multiple Git accounts, Windows Pageant setups, and CI runners using injected keys.

The useful mental model is simple. The server is not saying, "you are forbidden from this repo." It is saying, "I do not recognize the key presented for this connection."

That is why the error shows up after laptop migrations, fresh OS installs, account switching, or changing a remote from HTTPS to SSH. On Windows, I also see it when Pageant is offering an old key while OpenSSH is configured for a different one. In CI/CD, the usual culprit is a missing deploy key, a malformed private key secret, or a job container that never starts an agent.

Why developers misread it

The message sounds like an access-control problem, so people start by checking repo membership. Sometimes that matters later. Usually it does not matter yet.

SSH authentication comes first. Authorization comes after the server has matched your private key to a known public key. If that match never happens, the Git host cannot even get to the question of what repos you should be allowed to access.

That is the frame for the rest of the workflow. Verify which identity your machine is offering, confirm the host knows the matching public key, then check the environment-specific gotchas.

Generate and Locate Your SSH Keys

A lot of wasted time starts here. Developers jump straight to repo settings or agent config, but the actual problem is simpler: the machine either does not have the right keypair, or you are about to upload the wrong file.

Start by checking what already exists:

ls -la ~/.ssh

Look for pairs such as id_ed25519 and id_ed25519.pub, or older RSA files like id_rsa and id_rsa.pub. The file without .pub is the private key. The file with .pub is the public key.

A person typing on a computer keyboard displaying SSH directory contents on a desktop monitor screen.

That distinction matters because Git hosts only want the public half. If you paste the private key into a host, a secret store, or a chat window, you have created a security incident, not fixed authentication.

Know which file is safe to share

The public key is safe to register with GitHub, GitLab, or Bitbucket. Its contents usually begin with ssh-ed25519 or ssh-rsa and end with a comment such as your email or machine name.

The private key stays local. Keep it on the machine that will authenticate, and treat it like a password that cannot be rotated casually if it leaks across several systems.

If you are unsure which file to upload, use the one ending in .pub.

Generate a fresh key if needed

If ~/.ssh is empty, or the existing keys belong to an old laptop, old employer, or different Git account, generate a new keypair instead of guessing. Ed25519 is the default choice on modern systems because it is smaller, faster, and well supported. RSA 4096 is still useful for older environments.

Use:

ssh-keygen -t ed25519

If Ed25519 is not supported in your environment, use:

ssh-keygen -t rsa -b 4096

Accept the default filename if this is your only Git identity. Use a custom filename if you intentionally keep separate keys for work, personal repos, or automation. That extra organization helps later when SSH is choosing between multiple identities.

On Windows, check which SSH stack you are using before generating more keys. Git for Windows, Windows OpenSSH, WSL, and Pageant can all point at different key locations. I have seen developers generate a key in WSL, then test Git in PowerShell, where SSH never even looks at that file.

Confirm the public key contents

After generating the key, print the public half and inspect it:

cat ~/.ssh/id_ed25519.pub

Copy the full line exactly as shown. Do not trim the key type at the beginning. Do not remove the comment at the end. A partial paste often looks fine in the UI but will never match during authentication.

If you created a custom filename, read that file instead of id_ed25519.pub.

A quick check helps avoid the next common mistake. The private key proves identity. The public key is what the Git host stores. The fingerprint is what you compare later if you need to verify that the uploaded key matches the local one. Getting that mapping right saves a lot of guesswork once you start testing the connection.

Add Your Public Key to a Git Host

A very common dead end looks like this: the key exists locally, ssh-keygen worked, and Git still says Permission denied (publickey). At this point, stop generating new keys. Confirm that the exact public key from this machine is registered on the exact Git account the server expects.

A guide infographic showing how to add SSH public keys to GitHub, GitLab, and Bitbucket accounts.

GitHub, GitLab, and Bitbucket paths

The menu names shift over time, but the destination is usually your account-level SSH Keys page.

  • GitHub. Open Settings > SSH and GPG keys > New SSH key.
  • GitLab. Open Preferences or User Settings > SSH Keys.
  • Bitbucket. Open Personal settings > SSH keys.

Paste the full contents of the .pub file as one line. Do not paste the private key. Do not paste only the fingerprint. Use a label you will recognize later, such as laptop, workstation, WSL, or build-agent.

That label matters more than people expect. If you keep separate keys for work, personal, and automation, a clear label makes it obvious which identity the host should accept and which one you need to debug.

Verify the key you uploaded

The two mistakes I see most are simple. The wrong key gets uploaded, or the right key gets added to the wrong account or workspace.

Check the fingerprint of the public key on your machine:

ssh-keygen -lf ~/.ssh/id_ed25519.pub

Then compare it to the fingerprint shown by GitHub, GitLab, or Bitbucket for the saved key. If they do not match, the server is rejecting a different identity than the one you think you uploaded.

This is also where Windows setups can get confusing. A key copied from WSL is not the same as a key generated in PowerShell. If Git is running through Git for Windows or Pageant, it may be offering a different identity than the one you pasted into the host UI.

Check the remote URL before blaming SSH

If the repository remote uses HTTPS, SSH keys are irrelevant. I check this early because it saves time.

git remote -v

An SSH remote looks like this:

git@github.com:user/repo.git

If your remote points to https://..., update it:

git remote set-url origin git@github.com:user/repo.git

This mismatch shows up often after someone clones with HTTPS from the web UI, then later adds an SSH key and expects pushes to start working. In CI/CD, it also happens when a local repo uses SSH but the pipeline checkout step still uses an HTTPS credential helper.

Configure the SSH Agent and File Permissions

A very common failure pattern looks like this: the public key is already on GitHub or GitLab, the remote URL is correct, and SSH still answers with permission denied (publickey). At that point, the problem is usually local. Your machine is either not offering the right private key, or OpenSSH is refusing to use it.

Start with the agent, because it answers a simple question fast: what key will SSH present?

Make sure the SSH agent is actually running

Check whether an agent socket exists:

echo $SSH_AUTH_SOCK

If that prints nothing, start the agent:

eval "$(ssh-agent -s)"

Then load the key you expect Git to use:

ssh-add ~/.ssh/id_ed25519

Verify what is loaded:

ssh-add -l

If your key is missing here, SSH may try a different identity or none at all. On a machine with several keys, that distinction matters more than people expect.

Windows deserves a separate callout. Git for Windows, OpenSSH, WSL, and Pageant can all be in the path at once. I have seen Pageant offer an old key while ssh-add -l in PowerShell shows the new one, which makes the setup look correct until the handshake fails. If you are debugging on Windows, make sure the Git client and the agent belong to the same toolchain before changing anything else. If you want a quick refresher on AI tools for developer troubleshooting, use that after you confirm which SSH binary Git is calling, not before.

Fix permissions before chasing obscure causes

OpenSSH checks permissions aggressively. That is by design. Private keys that other users can read are not trusted for secure SSH connections, so SSH may ignore them without much ceremony.

Use these settings:

File / Directory Permission Command
~/.ssh 700 chmod 700 ~/.ssh
Private key 600 chmod 600 ~/.ssh/id_ed25519
Public key 644 chmod 644 ~/.ssh/id_ed25519.pub

If you keep a ~/.ssh/config, protect that too:

chmod 600 ~/.ssh/config

On macOS and Linux, this is a routine fix. In containers and CI jobs, it is often the actual cause because keys get mounted with broad defaults.

Check ownership on shared machines, servers, and CI runners

Permissions can be correct and still fail if the files belong to the wrong user. That shows up on build agents, dev containers, and systems where someone ran setup commands with sudo.

For a repository owned by the wrong account, fix it:

chown -R user:user .git/

If the SSH directory itself came from a copied home folder or a mounted secret, inspect that too:

ls -ld ~/.ssh
ls -l ~/.ssh

A CI pipeline has another wrinkle. Some runners do not start an agent by default, and some inject the key as a file but never load it. In that case, both steps are required: start the agent, add the key, then verify the file permissions inside the job, not on your laptop.

What to check first, in order

Work through these in order:

  • Confirm an SSH agent is running.
  • Add the exact private key you expect with ssh-add.
  • List loaded identities and make sure the fingerprint matches your intended key.
  • Fix permissions on ~/.ssh, the private key, and ~/.ssh/config if present.
  • Check file ownership on shared systems, containers, and CI runners.
  • On Windows, rule out Pageant or WSL using a different key than Git for Windows.

That order saves time because it moves from the common local failures to the stranger environment-specific ones. If all of this looks right and Git still fails, the next step is to inspect the SSH handshake itself.

Advanced Diagnostics for Stubborn Cases

When the obvious fixes don't solve permission denied publickey in Git, the fastest path forward is to stop guessing and inspect the handshake.

A professional developer sitting at a desk and debugging SSH connection errors on his computer screen.

Read the verbose SSH output

Run:

ssh -vT git@github.com

Substitute the host as needed. The useful lines usually mention whether SSH is offering a key, whether the server accepts it, or whether there are no more authentication methods to try.

What you're looking for is simple:

  • If you see your expected key being offered, your local config is at least pointing at the right file.
  • If the server accepts it, the handshake succeeded.
  • If authentication fails before that, the problem is still local identity or host registration.

This is one of the best ways to learn SSH instead of cargo-culting fixes.

Watch for the offered key path. If SSH is offering id_rsa but your registered key is id_ed25519_work, you've already found the mismatch.

Use ~/.ssh/config to force the right identity

Multiple keys are normal now. Work and personal GitHub accounts, a GitLab server at a client, a deploy key for CI, and maybe a self-hosted forge. SSH won't always guess correctly.

A minimal config can make the choice explicit:

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes

The important piece is IdentityFile, and it should point to the private key, not the public key. IdentitiesOnly yes helps when your agent is loaded with several keys and SSH keeps offering the wrong one first.

For a deeper refresher on secure SSH connections, it's worth understanding how host rules, identity files, and ports interact. Those basics explain a lot of "it works on one machine but not another" behavior.

If you're exploring more developer tooling around build and deployment workflows, Appjet AI is another resource to bookmark.

Handle blocked port 22 in corporate networks

Some office and enterprise networks block SSH on port 22. In that case, the key can be correct and the host registration can be correct, but the connection still never completes.

For GitHub, use port 443 in your SSH config:

Host github.com
HostName ssh.github.com
Port 443
User git

This preserves the SSH authentication flow while routing over a port that's often allowed through restrictive firewalls.

Windows-specific Pageant conflicts

Windows adds one of the most annoying edge cases. A frequent cause after key registration is stale key precedence from PuTTY/Pageant. If Pageant loads keys before the standard SSH agent, Git can end up using the wrong identity even when the right key exists on disk and is already registered. Unloading stale keys with pageant -u or explicitly setting IdentityFile in ~/.ssh/config often fixes it, as noted in this long-running Stack Overflow troubleshooting thread (Windows Pageant conflict notes).

This is the kind of issue that makes developers think the server is broken. It usually isn't. Windows just has more than one agent pathway competing to answer.

Context-Specific Fixes and Next Steps

By this point, the usual workstation problems are out of the way. The failures that remain tend to be tied to context: a CI runner with no agent, a deploy key attached to the wrong repo, or a machine using a different identity than the one you tested locally.

An infographic titled Advanced SSH Key Management for Special Contexts illustrating best practices for CI/CD, key management, and security.

CI and automation

CI/CD failures are usually simpler than they look. The runner either does not have the private key, cannot read it, or is presenting a different key than the one registered with the Git host.

Start by treating the job like a fresh machine. Print the remote URL. Confirm which user the job runs as. Run SSH in verbose mode inside the pipeline so you can see whether the client offers a key at all, and whether the server rejects it before repository permissions even come into play.

Use a dedicated automation key whenever possible. Reusing a developer's laptop key works in a pinch, but it creates cleanup problems later. Rotating that key can break both a person and a pipeline at the same time, and audit trails get muddy fast.

Store the private key in your CI platform's secret manager. Write it to disk at runtime, lock down permissions, and load only that key for the job. Teams that ship often across app code and deployment infrastructure usually benefit from documenting that setup alongside the rest of their delivery process, such as this guide to shipping a full-stack app quickly.

Deploy keys, machine users, and multiple identities

This is another place people lose time.

A deploy key usually grants one repository access. A machine user can span multiple repos. Personal keys follow an individual account. If the key is valid but attached to the wrong access model, Git still answers with the same frustrating permission denied (publickey) message.

On machines with several SSH identities, force the choice in ~/.ssh/config instead of hoping the agent picks the right one:

Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

Then point the remote at that host alias. This avoids the common case where the client offers three keys, the server rejects them, and SSH never gets to the one you intended.

If you need to unblock work right now

Switch the remote to HTTPS and use a token if your host supports it. That gets a release or hotfix out without spending another hour inside SSH logs.

Treat that as a temporary bypass, not the final fix. If SSH matters for your team's day-to-day workflow, come back and fix the identity path properly so the same issue does not return on the next machine, runner, or teammate account.

A few judgment calls that matter

One SSH key or several

One key is simpler. Several keys are easier to revoke, audit, and reason about. I usually recommend separate keys for personal work, company work, and automation because the operational cost is low once ~/.ssh/config is set up.

SSH or HTTPS

SSH is efficient once it is configured correctly. HTTPS is often easier inside locked-down networks, shared machines, and some enterprise environments. Choose the method your team can support consistently, not the one that sounds cleaner in theory.

What to check when nothing obvious is wrong

Look for the odd cases. A repository remote may point to a different host than expected. The CI job may be injecting line endings incorrectly into the private key. A Windows machine may still be using Pageant or PuTTY tooling even though you tested with OpenSSH. These are all real failure modes, and they produce the same generic error.

The pattern across all of them is the same. Git is attempting an identity handshake, and something in that chain does not match. Once you check the connection path in order, key on disk, key loaded, key offered, key registered, repository access, the error stops being mysterious and starts being mechanical.