Fixing the SSH error "Permission denied (publickey)"

Published on 19 min read

Your SSH login fails with Permission denied (publickey)? Seven causes, from file permissions and AllowUsers through to SELinux, each with the exact log line and the matching fix.

The connection attempt dies after a second, no password prompt appears, and all you get is a single line: Permission denied (publickey). This message is so unpleasant because it deliberately gives nothing away. The SSH server does not tell a potential attacker whether the user exists, whether the key was wrong, or whether the file is unreadable. That same reticence hits you as well, though, when you are standing at the door legitimately.

The good news: the possible causes are finite, they can be checked in a fixed order, and in most cases it is simply the file permissions. This article walks through the causes in order of how often they occur, shows the exact wording each one produces in the log, and explains how ssh -vvv lets you decide within thirty seconds whether the problem sits on your machine or on the server.

What the message actually means

The parentheses at the end are not decoration, they carry the most important information in the whole line. They list the authentication methods the server still offers after the failed attempt:

Permission denied (publickey).
Permission denied (publickey,password).
Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

If it says only publickey, password login is switched off on the server. If password is in the list, a password login would have been possible in principle, it simply was not attempted or failed as well. The variant with gssapi is typical for AlmaLinux, Rocky Linux and RHEL, where Kerberos support is compiled in.

It is important to separate this from messages that look similar but describe a completely different problem:

  • Permission denied, please try again. without parentheses means a wrong password, not a key problem.
  • Host key verification failed. concerns the server key in your known_hosts, not your own key.
  • Received disconnect from 203.0.113.7 port 22:2: Too many authentication failures means your agent offered too many keys one after another and the server gave up after MaxAuthTries.
  • Connection refused or a timeout are network or firewall matters. If you have just been tinkering with a UFW firewall or with Fail2ban, start there.

The fork in the road: reading ssh -vvv correctly

Before you change anything, let SSH show you what happens. Three v are deliberate, because with a single v the decisive lines are missing:

ssh -vvv deploy@203.0.113.7

The output is long, but there are only four places you need to look at. First, the user name the connection is actually made with:

debug1: Authenticating to 203.0.113.7:22 as 'deploy'

Second, which keys the client even considers, and third, which one it really sends:

debug1: Will attempt key: /home/tom/.ssh/id_ed25519 ED25519 SHA256:8Qk... agent
debug1: Offering public key: /home/tom/.ssh/id_ed25519 ED25519 SHA256:8Qk... agent
debug1: Authentications that can continue: publickey
debug1: No more authentication methods to try.

And fourth, the success message, which is exactly what is missing when things go wrong:

debug1: Server accepts key: /home/tom/.ssh/id_ed25519 ED25519 SHA256:8Qk...
debug1: Authenticated to 203.0.113.7 ([203.0.113.7]:22) using "publickey".

From this follows the fork in the road that saves you half the troubleshooting:

  • No Offering public key line with your key appears. Then the problem is on your machine, the key was never sent. Jump to cause 3.
  • Offering public key appears, but it is followed by Authentications that can continue again. Then the server saw your key and rejected it. Those are causes 1, 2, 4, 5 and 6, all on the server side.
  • send_pubkey_test: no mutual signature algorithm appears. Then the key type is the issue, jump to cause 7.

Two further lines from the debug output are worth a look. debug3: no such identity: /home/tom/.ssh/id_rsa: No such file or directory is harmless, the client simply works through all the default names. Permissions 0644 for '/home/tom/.ssh/id_ed25519' are too open. on the other hand is a real hit, because your private key is then ignored.

The server side: sshd -T and the logs

ssh -vvv shows the client's view and nothing else. Why the server rejected you is recorded only in the server log. As long as you still have an open session or can get in through the console in the customer panel, look there first.

On Debian and Ubuntu the service is called ssh, on AlmaLinux, Rocky Linux and RHEL it is called sshd. That is a classic trap when copying commands:

journalctl -u ssh -n 50 --no-pager      # Debian, Ubuntu
journalctl -u sshd -n 50 --no-pager     # AlmaLinux, Rocky, RHEL

The classic text file no longer exists everywhere. Ubuntu 22.04 and 24.04 still keep /var/log/auth.log in the server installation, because rsyslog ships with it. Debian 12 and Debian 13 no longer pull rsyslog into a minimal installation, so the file simply does not exist there and everything ends up in the journal. On the Red Hat family the file is called /var/log/secure. If you want the text file back on Debian, install rsyslog afterwards, on Debian and Ubuntu with apt-get install -y rsyslog, on the Red Hat family with dnf install -y rsyslog.

The second server command is even more important, because it prints the configuration that is actually in effect and resolves all included files while doing so:

sshd -T | grep -Ei 'pubkeyauth|authorizedkeysfile|strictmodes|permitrootlogin'

If the command answers with nothing but Missing privilege separation directory: /run/sshd instead of a configuration and prints not a single line, then a runtime directory is simply missing. This happens on Debian 11, Debian 12, Ubuntu 22.04 and Ubuntu 24.04 right after the package installation, as long as the service has never been started, and it happens in containers. In normal operation the systemd unit creates the directory itself via RuntimeDirectory=sshd. If the message appears, a preceding mkdir -p /run/sshd helps, after which sshd -T cleanly returns permitrootlogin, pubkeyauthentication yes, strictmodes yes and authorizedkeysfile. Debian 13 with OpenSSH 10 and the entire Red Hat family no longer have this limitation. Watch out for the message getting lost in a pipe: sshd -T | grep ... then shows nothing but empty output, and the actual return value 255 disappears inside the grep.

And if you really want to see what the server thinks without touching the running service: start a second instance in debug mode on a free port. It exits by itself after one connection and it cannot lock you out.

/usr/sbin/sshd -ddd -p 2222

From your own machine you then run ssh -p 2222 deploy@203.0.113.7, and the rejection appears in plain text in the server's terminal. The port has to be open in the firewall for this, of course.

Cause 1: permissions and ownership, by far the most common case

OpenSSH has the option StrictModes yes active by default. The server refuses to read a key from a file that anyone other than the user themselves could write to. That is not harassment, it stops another user from simply writing their own key into your authorized_keys.

The target state is narrowly defined:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 750 ~
chown -R "$(id -un):$(id -gn)" ~/.ssh

You can verify it in a single line:

stat -c "%a %U %G %n" ~ ~/.ssh ~/.ssh/authorized_keys

Expected are 750 or 700 for the home directory, 700 for .ssh and 600 for authorized_keys, plus your own user name in all three lines. The decisive point: the home directory must not be writable by group or others, so 770 or 777 is already enough to make it fail.

One number must not be misread as an error here. On AlmaLinux, Rocky Linux and Oracle Linux, /root carries the permissions 550, not 700 as on Debian and Ubuntu. stat -c reports that correctly, and it is perfectly fine. For StrictModes the only thing that counts is that group and others have no write permission, and 550 does exactly that. Anyone who follows up with a chmod 700 /root here has not repaired the login, only buried the real cause deeper.

The server log is then very explicit:

Authentication refused: bad ownership or modes for directory /home/deploy/.ssh
Authentication refused: bad ownership or modes for file /home/deploy/.ssh/authorized_keys
error: Could not open authorized keys '/home/deploy/.ssh/authorized_keys': Permission denied

Two details that other guides like to leave out. First the owner: if you created the file with sudo nano ~/.ssh/authorized_keys, it belongs to root and not to the user, and the login fails despite perfect 600 permissions. Second, sshd checks the entire path upwards. If the home directory does not sit under /home but somewhere like /srv/kunden/deploy, then /srv and /srv/kunden have to belong to root or to the user as well, and they must not be writable by group or others.

Cause 2: the wrong user name

A user that does not exist produces exactly the same message as a wrong key, because the server deliberately does not reveal which of the two cases applies. In the log the difference is obvious immediately:

Invalid user deply from 203.0.113.7 port 51234

The most common trigger is that the key sits in /root/.ssh/authorized_keys while you log in as a regular user, or the other way around. On ready-made cloud images the root login is often disabled and a prepared user exists instead, depending on the distribution debian, ubuntu, almalinux or rocky. With the standard images on a KernelHost root server, by contrast, you log in directly as root.

Also check whether your ~/.ssh/config is quietly substituting a different user. The following command does not open a connection, it only shows which settings really apply to this target:

ssh -G deploy@203.0.113.7

In the output, the interesting entries are user, hostname, port and the list of identityfile entries.

Cause 3: the key is never offered at all

If no Offering public key line with your key shows up in ssh -vvv, the server never got a chance. There are four typical reasons for that.

The key has a name of its own

OpenSSH only tries the default names id_ed25519, id_ecdsa and id_rsa automatically. A key called id_produktion is used only if you name it explicitly:

ssh -i ~/.ssh/id_produktion -o IdentitiesOnly=yes deploy@203.0.113.7

IdentitiesOnly=yes is not just decoration here. Without this option ssh additionally offers every key from the agent, and after too many attempts the server aborts with Too many authentication failures before the right key ever gets its turn.

The agent does not have the key

ssh-add -l

If the command answers with The agent has no identities. or Could not open a connection to your authentication agent., load the key with ssh-add ~/.ssh/id_ed25519.

Permissions on the private key

The private key has to be 600, otherwise the client refuses to use it. On Windows chmod has no effect, there you work through ACLs:

icacls %USERPROFILE%\.ssh\id_ed25519 /inheritance:r /grant:r "%USERNAME%":R

The authorized_keys file is broken

A public key is exactly one line. Copying it through editors, ticket systems or chat windows happily turns it into a line break in the middle of the Base64 block, and then nothing fits any more. Count the lines:

grep -c '^ssh-' ~/.ssh/authorized_keys
awk '{print NR": "NF" Felder, Typ "$1}' ~/.ssh/authorized_keys

Every line has to start with ssh-ed25519, ssh-rsa or ecdsa-sha2- and consist of two to three fields. The number of lines has to match the number of keys. A second classic is that the private key was entered by mistake instead of the public one, recognizable by BEGIN OPENSSH PRIVATE KEY. And a key in PuTTY format (.ppk) does not work like this, it has to be exported to OpenSSH first.

Whether the private and the public key belong together is settled by comparing fingerprints:

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

The same SHA256 value has to appear in both outputs. What the whole thing looks like when it is set up cleanly is described in our article on hardening SSH and setting up key login.

Cause 4: PubkeyAuthentication is off

Rarer, but then very clear-cut. Do not check the configuration file, check the result:

sshd -T | grep -i pubkeyauthentication

There is a trap lurking here that costs many hours. Debian from version 12 and Ubuntu from 22.04 carry the line Include /etc/ssh/sshd_config.d/*.conf right at the top of /etc/ssh/sshd_config. With sshd the rule is: for every keyword, the value found first wins. Because the include sits at the very beginning, every little thing in sshd_config.d beats the main file, no matter what is written further down there. If your change stays without effect, look in that directory:

grep -rniE 'pubkeyauthentication|authorizedkeysfile|allowusers|allowgroups' /etc/ssh/

The second point in this category is AuthorizedKeysFile. The defaults are .ssh/authorized_keys and .ssh/authorized_keys2. Some hardening scripts set the path to something like /etc/ssh/authorized_keys/%u. After that your file in the home directory is ignored completely, without any error message at all. sshd -T shows this too.

Cause 5: AllowUsers, AllowGroups and Match take effect

These directives cut off entire groups of users, and they do so before the key is even checked. The wording in the log:

User root from 203.0.113.7 not allowed because not listed in AllowUsers
User deploy from 203.0.113.7 not allowed because none of user's groups are listed in AllowGroups
User root from 203.0.113.7 not allowed because "PermitRootLogin no"

Remember the order of precedence: DenyUsers beats AllowUsers, and as soon as AllowUsers is set at all, every user not listed is locked out. With AllowGroups the group membership has to match, which you check with id deploy.

With PermitRootLogin the distinction matters: prohibit-password allows the root login with a key. Only no locks root out completely. In the output of sshd -T, however, do not expect the word prohibit-password: what appears there is the older, equivalent name without-password. And the default value is by no means the same everywhere, which regularly causes confusion when two servers are compared:

SystemValue from sshd -T
Debian 11, 12, 13without-password
Rocky Linux 9, Oracle Linux 9without-password
AlmaLinux 9, AlmaLinux 10yes

So on AlmaLinux root may also get in with a password, on the other systems listed it may not. If you move a service from AlmaLinux to Debian and have been logging in as root with a password so far, you land on exactly Permission denied (publickey) afterwards.

If the rule sits inside a Match block, the option that evaluates the configuration for one concrete case helps:

sshd -T -C user=deploy,host=client.example.com,addr=203.0.113.7 | grep -Ei 'pubkeyauth|allowusers|permitrootlogin'

That is the most reliable way to see what applies to exactly this user coming from exactly this IP address.

Cause 6: SELinux on AlmaLinux, Rocky and RHEL

On the Red Hat family SELinux runs in Enforcing mode by default, on Debian and Ubuntu it plays no role. The sshd process may read authorized_keys only if the file carries the context ssh_home_t. That is the case when it was created normally inside the home directory. It is not the case if you fetched it out of /tmp with mv or created the home directory by hand, because mv takes the old context along with it.

getenforce
ls -Z ~/.ssh

Correct is an entry that ends in ssh_home_t. If it says user_tmp_t or user_home_t, you have found the cause. The repair:

restorecon -R -v ~/.ssh

This step applies to the Red Hat family only. On Debian and Ubuntu there is no default SELinux setup, so the shell answers with restorecon: command not found, and that is not an error, it is simply not applicable. But even on AlmaLinux and Rocky Linux the command is missing in a slim installation, because the package for it is not part of the set. In that case install it first:

dnf install -y policycoreutils

The evidence is in the audit log, where the rejection is spelled out:

ausearch -m avc -ts recent

If the home directories sit in an unusual place, restorecon on its own is not enough, because SELinux does not recognize the path as a home directory at all. In that case you register the equivalence once and restore afterwards:

semanage fcontext -a -e /home /srv/kunden
restorecon -R -v /srv/kunden

semanage lives in the package policycoreutils-python-utils. Do not switch SELinux off to get the login working, that fixes a two-command problem with a system-wide loss of security.

Cause 7: old server, wrong key type

Since OpenSSH 8.8 the client rejects RSA signatures with SHA-1. Ubuntu 22.04 is already affected, and Debian 13 now ships OpenSSH 10. If you want to reach a very old server from such a current system, one that only speaks the old ssh-rsa, the debug output shows:

debug1: send_pubkey_test: no mutual signature algorithm

This is not a permission problem, your key is perfectly fine. For one-off access this helps:

ssh -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa deploy@203.0.113.7

As a permanent setting it belongs in ~/.ssh/config under a Host entry, so that it only affects this one server. On very old clients the option is still called PubkeyAcceptedKeyTypes. The real solution is to update the old server, because from OpenSSH 7.2 onwards it speaks the SHA-2 variants as well, and your existing RSA key then keeps working unchanged. Only the signature method changes.

The reverse case exists too. An ed25519 key needs at least OpenSSH 6.5 on both sides, hardware tokens of type ed25519-sk at least 8.2. And DSA is history: since OpenSSH 10.0 ssh-dss has been removed completely, so legacy keys of that kind no longer work against Debian 13 at all. Which types your client knows is shown by:

ssh -Q key

If ssh is missing entirely on a freshly installed AlmaLinux, Rocky Linux or RHEL, that comes down to a trap in the package names: dnf install openssh-server only brings the service, no client tools. Without them you lack ssh, ssh-add and therefore ssh -Q and ssh -G as well. It is installed with a plural s, unlike the Debian package openssh-client:

dnf install -y openssh-clients

On AlmaLinux, Rocky and RHEL 9 a second layer comes on top. There, system-wide crypto policies also govern what is allowed, independently of the sshd configuration:

update-crypto-policies --show

This tool is purely on the Red Hat side as well, on Debian and Ubuntu it does not exist. And even on Oracle Linux 9 it is missing in a minimal installation, where dnf install -y crypto-policies-scripts adds it, after which DEFAULT comes back as expected. If it says DEFAULT, SHA-1 signatures are already blocked system-wide. update-crypto-policies --set LEGACY solves that, but it weakens the entire machine and should at most be a stopgap during a migration.

If you have locked yourself out

The dangerous moment is not the error itself, it is the repair work on sshd_config. Three rules that make locking yourself out practically impossible:

  1. Keep a second session open. Restarting sshd does not drop existing connections. As long as one terminal stays open, you can undo every change.
  2. Check the syntax before every restart. sshd -t prints the line number when something is wrong and stays quiet when everything fits. Otherwise a typo in the configuration prevents the service from starting, and then nobody gets in any more. If you get Missing privilege separation directory: /run/sshd instead, your configuration is fine and only the runtime directory is missing, see above.
  3. Test from the second session before you close the first one.

Restarting differs between systems. On Debian and Ubuntu the unit is called ssh, on the Red Hat family sshd. Since Ubuntu 22.10 and in Debian 13, SSH is additionally started through socket activation: the configuration from sshd_config still applies, but a changed Port setting only takes effect once ssh.socket has been restarted as well.

sshd -t
systemctl restart ssh          # Debian, Ubuntu
systemctl restart ssh.socket   # additionally, if the port was changed
systemctl restart sshd         # AlmaLinux, Rocky, RHEL

If it does happen anyway, you need a way past SSH. On a KernelHost root server you open the VNC console in the customer panel and log in there with the root password, entirely without a network service. If that does not get you anywhere, for example because sshd no longer starts at all, the rescue system helps: you boot into an emergency environment, mount the system file system and correct authorized_keys and the permissions directly on disk. Remember to check the ownership after mounting, because in the rescue system you are root and would otherwise create files with the wrong owner.

How to tell that it really works

The fact that a login succeeds does not yet mean it ran over the key. As long as password login is active, the server can quietly let you fall back to it. The honest test rules out every other method:

ssh -o BatchMode=yes -o PreferredAuthentications=publickey deploy@203.0.113.7 'id -un; hostname'

BatchMode=yes suppresses every interactive prompt. If your user name and the hostname come back and echo $? afterwards returns a 0, then the key alone did the work.

The second piece of evidence is in the server log, and it even names the fingerprint of the key that was used:

Accepted publickey for deploy from 203.0.113.7 port 51234 ssh2: ED25519 SHA256:8Qk...

Compare this SHA256 value with the output of ssh-keygen -lf ~/.ssh/id_ed25519.pub. If the two match, you not only know that the login works, but also which key did it. That matters when several keys are in play and you want to withdraw one of them.

The order to work through

If you have no time for theory, work down this list from top to bottom. It is sorted by frequency, not by elegance.

  1. Run ssh -vvv and establish whether Offering public key appears. That splits the problem into client and server.
  2. Check the permissions: 700 on ~/.ssh, 600 on authorized_keys, home directory not group-writable, everything owned by the user.
  3. Check the user name, when in doubt ask ssh -G and search the log for Invalid user.
  4. Compare the fingerprints of the private key and authorized_keys, and check the number of lines in the file.
  5. Evaluate sshd -T: pubkeyauthentication, authorizedkeysfile, strictmodes, permitrootlogin.
  6. Check for AllowUsers, AllowGroups, DenyUsers and Match blocks, including the sshd_config.d directory.
  7. On the Red Hat family run ls -Z ~/.ssh and if needed restorecon -R -v ~/.ssh.
  8. Only for very old remote ends: extend the signature algorithms with PubkeyAcceptedAlgorithms=+ssh-rsa.

By this point at the latest the cause is found. If you then want to set the access up cleanly from scratch, our introduction to connecting via SSH and the checklist for a new root server are the right follow-ups.

Frequently asked questions

What does the list in parentheses in "Permission denied (publickey,password)" mean?
It lists the authentication methods the server still offers after the failed attempt. If it says only publickey, password login is switched off. If password is in the list, a password login would have been possible. The variant with gssapi-keyex and gssapi-with-mic is typical for AlmaLinux, Rocky Linux and RHEL.
Which permissions do ~/.ssh and authorized_keys need?
700 for ~/.ssh, 600 for ~/.ssh/authorized_keys, and the home directory must not be writable by group or others, so 750 or 700. All three have to belong to the respective user and not to root. You can check it with: stat -c "%a %U %G %n" ~ ~/.ssh ~/.ssh/authorized_keys
How do I tell from ssh -vvv whether the problem is on the client or on the server?
By the line "Offering public key". If it is missing, the client never sent your key and the problem is local (wrong file name, empty agent, permissions on the private key too open). If it appears and is followed by "Authentications that can continue" again, the server saw the key and rejected it, and then it comes down to permissions, user name, sshd configuration or SELinux.
Why does key login fail on AlmaLinux even though the permissions are correct?
Usually it is the SELinux context. The authorized_keys file has to carry the type ssh_home_t. If it was moved out of /tmp with mv, it keeps the old context and sshd is not allowed to read it. Check with ls -Z ~/.ssh, repair with restorecon -R -v ~/.ssh. If restorecon is missing in a slim installation, dnf install -y policycoreutils adds it. The evidence is in the audit log, available via ausearch -m avc -ts recent.
What does "no mutual signature algorithm" mean?
The client is OpenSSH 8.8 or newer and rejects RSA signatures with SHA-1, while the remote server only knows the old ssh-rsa. For one-off access the option PubkeyAcceptedAlgorithms=+ssh-rsa together with HostKeyAlgorithms=+ssh-rsa helps. The clean solution is to update the old server, after which your RSA key keeps working unchanged and only the signature method changes.
My change in /etc/ssh/sshd_config has no effect. Why is that?
On Debian from 12 and Ubuntu from 22.04 the line Include /etc/ssh/sshd_config.d/*.conf sits right at the top. Since sshd takes the first value it finds for each keyword, every file in that directory overrides the main file. What counts is always the output of sshd -T, not the content of the file.
How do I get back onto the server after locking myself out?
Through the VNC console in the customer panel you log in to the machine directly with the root password, entirely without SSH. If sshd no longer starts at all, you boot into the rescue system, mount the system file system and correct authorized_keys, permissions and ownership directly on disk.

SSH OpenSSH Troubleshooting Linux Server administration Authentication SELinux Debian Ubuntu AlmaLinux