DEV Community

Cover image for SSH Permission Denied (publickey): The Real Fix
Mr Say Nothing
Mr Say Nothing

Posted on Originally published at mrsaynothing.dev

SSH Permission Denied (publickey): The Real Fix

Originally published at mrsaynothing.dev.

At 02:10 a deploy died on a line I have typed a thousand times: ssh deploy@stagingPermission denied (publickey). The key was fine. The offer wasn't.

TL;DR: Permission denied (publickey) means every key the client offered was refused by the server — a negotiation verdict, not a password typo. Diagnose with ssh -vvv (which keys were offered) and the server log journalctl -u ssh (which keys were refused, and why). Then fix one of the four causes: wrong user, key missing from authorized_keys, permissions too open, or an ssh-rsa key refused by OpenSSH 8.8+. ssh-copy-id prevents most recurrences.

$ ssh -T git@github.com
git@github.com: Permission denied (publickey).
Enter fullscreen mode Exit fullscreen mode

The error is a verdict, not a hint: everything in the offer was refused.

Why does SSH say "Permission denied (publickey)"?

Public-key auth is an offer-and-refuse negotiation. The client proposes every key it can find — identities from your agent, your ~/.ssh/config, default filenames (id_ed25519, id_rsa) and anything passed with -i. The server checks each offer against the target account's ~/.ssh/authorized_keys. When none match, and password auth is disabled or already exhausted, the client prints the one line every devops engineer has memorised.

Two facts matter for debugging. First, the message does not say which user you hit — half of all cases are a perfectly good key sitting in the wrong account's authorized_keys. Second, the server already told you why: sshd logs every refused offer. The client-side stack trace nobody asked for and everybody needs is -vvv.

How do I find which key SSH is actually offering?

ssh -vvv deploy@staging 2>&1 | grep -iE "offering|identity|denied|authentications"
# debug1: Offering public key: /home/you/.ssh/id_ed25519 RSA-SHA256 (explicit)
# debug1: Authentications that can continue: publickey
# deploy@staging: Permission denied (publickey).
Enter fullscreen mode Exit fullscreen mode

Read three lines: Offering public key (what the client proposed), Authentications that can continue (what the server will still accept — if password is absent, no password prompt will ever come), and the final verdict. If your intended key never appears in the offer list, the problem is client-side: wrong -i path, an IdentityFile in ~/.ssh/config you forgot about, or an agent (ssh-add -l) holding a stale identity that answers first.

What are the four real causes?

Cause Telltale Fix
Wrong user Key works for root@host, fails for deploy@host Key must be in that user's ~/.ssh/authorized_keys
Key not installed Server log shows Failed publickey for every offer ssh-copy-id user@host
Permissions too open Client: WARNING: UNPROTECTED PRIVATE KEY FILE — the key is skipped, never offered chmod 700 ~/.ssh; chmod 600 ~/.ssh/*
Legacy ssh-rsa key Server runs OpenSSH 8.8+; old RSA key never accepted Re-generate as ed25519, or upgrade the key

The permissions cause deserves its own note, because OpenSSH enforces it hard: a private key that group or world can read is refused by the client itself and silently drops out of the offer. The fix is two commands:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

The ssh-rsa cause trips older CI images and Raspberry Pis: OpenSSH 8.8 (released 2021-10-01, openssh.com/txt/release-8.8) disabled ssh-rsa signatures — the SHA-1-based variant — by default. Per the release notes, "the ssh-rsa signature scheme is ... disabled by default". An RSA key generated in 2018 is not broken; its signature format is just no longer spoken. Generating an ed25519 key (ssh-keygen -t ed25519) is the durable answer, and cloud consoles give you a serial console when you lock yourself out mid-fix.

If sshd itself refuses to start while you are editing configs, that is a different hunt:


How do I see why the server refused the key?

The server-side log is the honest witness. On Ubuntu (systemd), sshd logs per-offer verdicts:

journalctl -u ssh -n 50 --no-pager | grep -iE "publickey|denied|accepted"
# sshd[4421]: Failed publickey for deploy from 203.0.113.7 port 51422 ssh2: RSA SHA256:...
# sshd[4421]: Accepted publickey for deploy from 203.0.113.7 port 51422 ssh2: ED25519 SHA256:...
Enter fullscreen mode Exit fullscreen mode

Failed publickey with a fingerprint you did not expect means the client offered a different key than you thought — back to -vvv. Failed publickey with your fingerprint means the key is right but not installed for that account, or the home-directory permissions on the server side are wrong (same 700/600 rule applies to the server's ~/.ssh and ~/.ssh/authorized_keys).

How do I install my key correctly?

ssh-copy-id deploy@staging
# Number of key(s) added: 1
ssh -o BatchMode=yes deploy@staging 'echo ok'   # non-interactive proof
Enter fullscreen mode Exit fullscreen mode

ssh-copy-id appends your public key to the target account's authorized_keys and sets sane permissions — it exists because hand-pasting into authorized_keys fails on a trailing newline or a missing directory exactly often enough. The BatchMode check is the real test: it disables all prompts, so success means the key alone carried the login. Once the key works, scp and rsync inherit the same auth for free — the choice between them is bandwidth, not credentials:


The 30-second triage

ssh -vvv user@host 2>&1 | grep -i offering      # 1. which keys were offered?
journalctl -u ssh -n 50 | grep -i publickey     # 2. why were they refused?
ls -ld ~/.ssh ~/.ssh/authorized_keys            # 3. 700 / 600?
ssh-copy-id user@host                           # 4. install, then verify with BatchMode
Enter fullscreen mode Exit fullscreen mode

Permission denied (publickey) is a negotiation result, not a password prompt — the server refused every key in the offer, and its log already knows why.

Run the four commands in order and the failure stops being mysterious: one of them names the culprit every time. Mine was a stale agent identity answering before the right key — ssh-add -d, and the deploy went green at 02:31.


More field notes at mrsaynothing.dev · code at GitHub · say hi: contact@mrsaynothing.dev

Top comments (0)