●moray: ssh, sftp & terminal

Fixing “Permission denied (publickey)”

The eight causes of this error in the order worth checking — from the wrong username to file permissions to the RSA algorithm modern OpenSSH now refuses.

Troubleshooting · Bartłomiej Żabicki · updated 2026-07-27 · 10 min read

deploy@web1.example.com: Permission denied (publickey).

Nine words, no clue. The good news is that this error is much narrower than it looks: the network worked, DNS worked, the SSH handshake completed, and the server told you exactly one thing — it will only accept a public key, and none of the ones you offered were acceptable.

That leaves a short list of causes. Here they are in the order worth checking, which is roughly the order of how often they turn out to be the answer.

First: make the client tell you what it did

Do not guess. Run the connection again with -v:

ssh -v deploy@web1.example.com

Three lines in that output answer most of the question:

debug1: Will attempt key: /Users/bart/.ssh/id_ed25519 ED25519 SHA256:AbCd...
debug1: Offering public key: /Users/bart/.ssh/id_ed25519 ED25519 SHA256:AbCd...
debug1: Authentications that can continue: publickey
  • No "Offering public key" line at all → the client never sent a key. The problem is on your machine: cause 1, 2 or 3 below.
  • "Offering" appears, then it moves on → the server saw your key and rejected it. The problem is on the server: cause 4, 5, 6 or 7.

That single distinction cuts the search space in half before you touch anything.

1. The username is wrong

Easily the most common cause, and the most embarrassing. authorized_keys is per-user: a key in /home/deploy/.ssh/authorized_keys does nothing for ssh root@host.

ssh -v deploy@web1.example.com    # not your local username by default

If you set User in ~/.ssh/config, confirm it is actually applying:

ssh -G web1 | grep '^user '

2. The client is not offering the key you think

Without an IdentityFile, SSH tries a fixed list of default names (id_ed25519, id_ecdsa, id_rsa…) plus everything in your agent. A key called ~/.ssh/prod_key is not on that list and will never be tried.

Host web1
    HostName web1.example.com
    User deploy
    IdentityFile ~/.ssh/prod_key
    IdentitiesOnly yes

IdentitiesOnly yes matters more than it looks — see cause 3.

3. Too many keys, and the server hung up first

Servers default to MaxAuthTries 6. Every key your client offers counts as an attempt, and the agent's keys are offered before the one you named. With seven keys loaded, the server disconnects before your correct key is ever tried — and the message is, of course, Permission denied (publickey).

In -v output this looks like several "Offering public key" lines followed by an abrupt disconnect.

ssh-add -l          # how many keys is the agent holding?
ssh-add -D          # clear them all, if you want to prove the point

Fix it properly with IdentitiesOnly yes on the host block, which limits SSH to the keys you explicitly named. More on the agent in ssh-agent explained.

4. File permissions on the server

OpenSSH refuses to read authorized_keys if the file, the .ssh directory, or the user's home directory is writable by anyone but the owner — and it does so silently from the client's point of view.

# on the server, as the target user
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 755 ~                       # home must not be group/world-writable
chown -R deploy:deploy ~/.ssh

The giveaway is in the server's log:

sudo journalctl -u ssh -n 30      # or /var/log/auth.log
# Authentication refused: bad ownership or modes for directory /home/deploy/.ssh

If you can read the server's auth log, check it early — it often names the cause outright.

5. The key is not actually in authorized_keys

Or it is, but mangled. A public key must be one line. Copy-paste through a chat client or an editor with hard wrapping frequently inserts a newline, which silently invalidates it.

# on the server: how many lines, and do they start correctly?
wc -l ~/.ssh/authorized_keys
cut -c1-20 ~/.ssh/authorized_keys

# compare fingerprints — client side
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# server side
ssh-keygen -lf ~/.ssh/authorized_keys

The second command prints a fingerprint per line. If yours is not among them, the key is not installed, whatever you think you pasted.

6. Your RSA key is fine; its signature algorithm is not

This one broke a lot of previously working setups. OpenSSH 8.8 (2021) disabled the ssh-rsa signature scheme by default, because it uses SHA-1. Your RSA key is not the problem — the signature algorithm negotiated with it is.

Symptom: an RSA key that worked for years stops working right after a server upgrade, and -v shows your key offered and refused.

The right fix is a new key:

ssh-keygen -t ed25519 -C "laptop-2026"

The temporary fix, if you cannot change the key yet, is to re-enable the old algorithm — on the server:

# /etc/ssh/sshd_config
PubkeyAcceptedAlgorithms +ssh-rsa
HostkeyAlgorithms +ssh-rsa

Treat that as a note-to-self with an expiry date, not a solution. Background in SSH keys explained.

7. The server is not looking where you think

Two settings move the goalposts:

# /etc/ssh/sshd_config
AuthorizedKeysFile .ssh/authorized_keys      # can be repointed elsewhere
PubkeyAuthentication yes                     # can be off entirely
AllowUsers bart deploy                       # anyone not listed is refused
AllowGroups sshusers                         # same, by group

A Match block can override any of these for specific users, addresses or groups, and its effects are easy to forget:

Match User backup
    PubkeyAuthentication no

Check what the server actually resolved:

sudo sshd -T | grep -Ei 'pubkeyauth|authorizedkeysfile|allowusers|allowgroups'

sshd -T prints the effective configuration the same way ssh -G does for the client. It is the fastest way to end an argument about what the config says.

8. SELinux has relabelled the file

On RHEL, Fedora, Rocky and friends, a .ssh directory created by an unusual path — restored from a backup, copied with the wrong tool, made by a provisioning script — can end up with the wrong security context. Everything looks right, and it still fails.

ls -Z ~/.ssh/authorized_keys      # want ssh_home_t
restorecon -R -v ~/.ssh

The order, condensed

  1. ssh -v — is the key even being offered?
  2. Right username?
  3. Right key, and is it named with IdentityFile?
  4. Too many keys hitting MaxAuthTries? → IdentitiesOnly yes
  5. Permissions on the server's ~, ~/.ssh, authorized_keys
  6. Key actually present and one line in authorized_keys
  7. RSA + OpenSSH 9 → generate Ed25519
  8. sshd -T for AllowUsers, AuthorizedKeysFile, Match blocks
  9. ls -Z if the distro uses SELinux

Nine times out of ten you stop at step 2, 4 or 5.

Keeping it from recurring

Most of these are avoidable with one habit: a named IdentityFile plus IdentitiesOnly yes in your SSH config for every host, so there is never ambiguity about which key is being offered to whom. A client that stores each host's key explicitly — as Moray does, with the key in the device Keychain — removes the whole category, because there is no "which of my eleven keys did it try" question to answer.

Written by Bartłomiej Żabicki

iOS developer writing Swift since 2015, with a homelab he administers from an iPhone more often than he would like to admit. Moray is the SSH client he wanted to exist.

Frequently asked

What does “Permission denied (publickey)” actually mean?
The server refused every authentication method your client offered and would only accept a public key. It is almost never a network problem — the connection worked and the handshake completed. Either the server has no matching key on file, could not read the file it lives in, or rejected the key's signature algorithm.
Why did my RSA key stop working after a server upgrade?
OpenSSH 8.8 disabled the ssh-rsa signature scheme, which uses SHA-1, by default. The key itself is fine — the signature algorithm is not. Either re-enable it explicitly with PubkeyAcceptedAlgorithms, or better, generate an Ed25519 key.
How do I see which key SSH is actually offering?
Run ssh -v and read the “Offering public key” lines. They list each key in order, followed by the server's response. If your key never appears, the client is not loading it; if it appears and is refused, the problem is on the server.

try it yourself

Moray does all of this natively

A real terminal, dual-pane SFTP, tunnels and jump hosts on iPhone, iPad and Mac. Keys stay in the Keychain, hosts sync over your own iCloud, and there is no account to make.