Fix "Permission denied (publickey)" and "Host key verification failed"
Fix Permission denied (publickey) and Host key verification failed: read ssh -vvv output, check keys and permissions, sshd logs and known_hosts fingerprints.
Published 12 min read
Permission denied (publickey) means the server did not accept any key your client offered. The parenthesized part lists the login methods the server still allows, and a bare (publickey) means keys are the only option. Host key verification failed means ssh could not verify the server's host key, so it stopped before logging in. Either the name is missing from known_hosts and ssh cannot ask (or is told not to), or the stored key differs from the one the server now offers.
Work through it in order
Read the debug output first, then check which account and key the client uses, then the local key files, then the server. Handle host keys last, because the fix there is a security decision, not a config change.
Start with the debug output
Ask ssh for its most detailed output, -vvv, and filter for the lines that matter:
ssh -vvv alice@server.example.com 2>&1 | grep -E 'Will attempt|Offering|Server accepts|Authentications that can continue|Authentication succeeded|denied'
On Windows, pipe to Select-String in place of grep. A failing run looks roughly like this. The wording varies a little between releases:
debug1: Will attempt key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:...
debug1: Offering public key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:...
debug1: Authentications that can continue: publickey
debug1: No more authentication methods to try.
alice@server.example.com: Permission denied (publickey).
Read it as three questions. Which keys went out? The "Will attempt" and "Offering public key" lines answer that. Agent keys are offered too unless IdentitiesOnly is set. Did the server accept one? A Server accepts key line means the server will accept that key for this account, and the signature check comes next. If you never see that line for the key you expect, the server does not accept it for this account. What does the server still allow? Authentications that can continue lists the remaining methods. If it lists only publickey, passwords are off, and the problem is the key, the account or the file.
The parenthesized list changes with the server's policy. Permission denied (publickey,password) means passwords are still accepted, so ssh prompts for one once the key fails. A bare Permission denied (publickey) means keys are the only method left. If the output has no Offering public key line at all, ssh had no identity to offer, which points at the client's configuration rather than the server.
Use the debug lines to choose where to look:
| What the debug output shows | What it usually means | Go to |
|---|---|---|
No Offering public key line | ssh had no identity to offer | Steps 1 and 2 |
An offer, but no Server accepts key | The server does not have that key for this account, or it rejected the files | Step 4 |
UNPROTECTED PRIVATE KEY FILE warning | Your private key has a group or other permission bit set | Step 3 |
Too many authentication failures | The agent offered more keys than the server allows attempts | Step 2 |
REMOTE HOST IDENTIFICATION HAS CHANGED | The stored host key no longer matches the server | Step 5 |
Step 1: Check the account and key ssh really uses
Before you blame the server, check what ssh will do. ssh -G prints the effective configuration after every Host and Match block is applied, without connecting:
ssh -G db01 | grep -E '^(user|hostname|port|identityfile|identitiesonly) '
The output uses lowercase keywords, such as user alice. A user typed on the command line, as in ssh bob@db01, overrides the User line in the config. A key that works for alice fails for bob, so compare the account first.
Step 2: Make sure the right key goes out
The agent holds keys, and ssh offers them one at a time. The server limits how many authentication attempts a connection gets: MaxAuthTries defaults to 6 (sshd_config(5)). With several keys loaded, the attempts run out before ssh reaches the right one, and the server disconnects with Too many authentication failures.
OpenSSH 10.6 changed how key checks are counted. Up to six public key queries, set by PubkeyAuthOptions max-pk-ok with a default of 6, do not count against MaxAuthTries. Further attempts after those queries count as failures. Earlier releases counted those queries against MaxAuthTries too, so the limit can be reached sooner on older servers. Either way, pin the key per host:
Host db01
HostName db01.internal.example.com
User alice
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
IdentitiesOnly yes limits ssh to the configured files, even when the agent holds others (ssh_config(5)). The SSH config guide covers the rest of the file format.
Step 3: Check the private key on your machine
ssh refuses a private key you own when any group or other permission bit is set on it. It prints an UNPROTECTED PRIVATE KEY FILE warning, says the key will be ignored, and carries on without it, so the failure looks like a server rejection. Check the modes and fix them:
ls -l ~/.ssh/
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_work
Keep private keys at 600 or stricter. Public keys do not need that protection.
Step 4: Check the server
One ordinary case is a rebuilt server. The account exists, but its home directory is new, and nobody copied authorized_keys across. Check that file first.
Modes and ownership
StrictModes is on by default. It checks the modes and ownership of the user's files and home directory before accepting a login (sshd_config(5)). The sshd(8) FILES section is more specific: if authorized_keys, the ~/.ssh directory or the home directory is writable by other users, sshd will not allow the file to be used unless StrictModes is set to no. Check these on the server:
ls -ld ~ ~/.ssh
ls -l ~/.ssh/authorized_keys
You want drwx------ on ~/.ssh and -rw------- on authorized_keys, and no group or other write bit on your home directory. Fix a writable home directory with chmod go-w ~, and make sure the files belong to the login account.
Settings that reject the key
sshd -T prints the effective configuration after Include processing. Match blocks apply only when you add -C with connection details, so the output shows the global settings (sshd(8)):
sudo sshd -T | grep -Ei '^(pubkeyauthentication|authorizedkeysfile|strictmodes|allowusers|denyusers|permitrootlogin|authenticationmethods) '
Look for pubkeyauthentication yes, an authorizedkeysfile that points where you expect, and an allowusers line that includes your account. AllowUsers allows logins only for the names it lists. If authenticationmethods requires a second method, a key alone is not enough, and the client shows the remaining method in its output.
What the log says
The log names the file or directory that failed a check, so read it before guessing:
sudo journalctl -u ssh -n 50 --no-pager # Debian and Ubuntu
sudo journalctl -u sshd -n 50 --no-pager # Fedora and RHEL
A StrictModes refusal looks like this (typical output):
sshd[2191]: Authentication refused: bad ownership or modes for directory /home/alice
On Debian with rsyslog, the same messages also go to /var/log/auth.log. Keep LogLevel at its default for everyday use. sshd_config(5) says that logging with a DEBUG level violates the privacy of users and is not recommended, so use it briefly, on a test server.
If the log tells you nothing, run a one-off debug daemon on another port. With -d, it stays in the foreground and handles one connection, so you can watch the exchange:
sudo /usr/sbin/sshd -d -p 2222
From the client, run ssh -p 2222 alice@server.example.com. The debug daemon exits when that connection closes.
SELinux and Windows servers
On Fedora and RHEL, a wrong SELinux label produces the same symptom even when the modes are right. ls -Z ~/.ssh shows the labels, and sudo restorecon -Rv ~/.ssh restores the defaults. The key setup guide covers the label in more detail. On a Windows server, an administrator account must use administrators_authorized_keys with the ACL described in that guide.
Step 5: Handle host key errors
Two host key problems look alike, and both end with Host key verification failed. An unknown host fails that way when ssh cannot ask, for example in a script, or when StrictHostKeyChecking is yes. A changed host fails because the key stored for that name no longer matches the server, and ssh prints REMOTE HOST IDENTIFICATION HAS CHANGED first.
What known_hosts protects
RFC 4251 (section 4.1) explains that the server's host key lets the client verify that it is talking to the correct server, which requires the client to know that key in advance. ssh stores the keys it has accepted in ~/.ssh/known_hosts. ssh(1) says that if a host's identification changes, ssh warns about it and disables password authentication, to prevent server spoofing and man-in-the-middle attacks. Treat a changed key as an alarm until you have checked it. A typical run looks like this, with some banner lines trimmed:
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Add correct host key in /home/alice/.ssh/known_hosts to get rid of this message.
Offending ED25519 key in /home/alice/.ssh/known_hosts:12
Host key for db01.internal.example.com has changed and you have requested strict checking.
Host key verification failed.
The boring explanations come first: a rebuilt server, a replaced cloud instance, or an address that now belongs to another machine. Until you have compared the fingerprint through a channel you trust, treat an impostor as possible.
Verify the new fingerprint through a channel you trust
On the server itself, through console access or a session you already trust, print the host key fingerprint:
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
The output looks like 256 SHA256:... root@db01 (ED25519). Compare that string with the fingerprint your client shows. Comparing it over the same connection you are trying to trust proves nothing, because an impostor would show its own fingerprint there.
RemoteOpsX shows the SHA-256 fingerprint of a server's host key before it trusts the host. On the desktop, a new key is trusted only after you confirm it, or when it matches a key you already trust, such as one in your OpenSSH known_hosts. The Android preview trusts a new key only after an explicit tap. On both, a changed key stays blocked until you replace the trusted identity after checking it out of band.
Remove the stale entry
After you verify the new key, remove the old entry for that name, and for the address if you ever connected by IP:
ssh-keygen -R db01.internal.example.com
ssh-keygen -R 203.0.113.10
ssh-keygen -R removes every key stored for the host, including hashed entries (ssh-keygen(1)). Its output names the line it removed and the file it rewrote, and the original file is kept as known_hosts.old:
# Host db01.internal.example.com found: line 12
/home/alice/.ssh/known_hosts updated.
Original contents retained as /home/alice/.ssh/known_hosts.old
For a non-default port, pass the name in the bracketed form that known_hosts uses, such as ssh-keygen -R "[db01.internal.example.com]:2222". Reconnect, compare the fingerprint the prompt shows with the value from the server, and only then type yes.
Choose StrictHostKeyChecking on purpose
ssh_config(5) defines the values below. The table shows what each does with a new host and with a changed one.
| Value | New host | Changed host | Use it for |
|---|---|---|---|
| ask (default) | Asks you to confirm, then stores the key | Refuses | Interactive work |
| accept-new | Stores the key without asking | Refuses | Scripts that meet new hosts |
| yes | Refuses unless the key is already in known_hosts | Refuses | Automation where you manage known_hosts |
| no (or off) | Stores the key and continues | Continues, with password, keyboard-interactive, agent and port forwarding turned off | A short test you control |
Scripts should not depend on no. If a job must reach hosts you have not seen, load their keys from a verified source first. ssh-keyscan gathers host keys without checking them, so it cannot tell you whether they are the right ones.
FAQ
Why does the key work from one computer and not another?
Each computer has its own agent, ~/.ssh/config and key files. Run ssh -G on both machines and compare the user and identityfile lines, then compare the fingerprints that ssh-add -l lists. The usual difference is a key or config entry that exists on only one machine.
Is it safe to run ssh-keygen -R?
Removing an entry makes ssh treat the host as unknown, so with the default ask setting the next connection asks you to confirm. That prompt is the moment to compare fingerprints, so never accept the new key without comparing it.
Can I turn off host key checking for a quick test?
Use -o StrictHostKeyChecking=no for one command, on a network you control, and treat that session as untrusted. A connection without checking gives no protection against an impostor, so keep the option out of config files.
Why does host key verification fail only in cron jobs?
A job that cannot prompt has nobody to answer for an unknown host, so ssh refuses. Load verified keys into known_hosts ahead of time, or use accept-new for new hosts. accept-new still refuses a changed key, which is what you want.
How verbose should server logging be?
Keep the default for normal operation. sshd_config(5) says DEBUG logging violates user privacy and is not recommended, so use it briefly on a test server only.
References
- ssh(1): https://man.openbsd.org/ssh
- ssh_config(5): https://man.openbsd.org/ssh_config
- sshd_config(5): https://man.openbsd.org/sshd_config
- sshd(8): https://man.openbsd.org/sshd
- ssh-keygen(1): https://man.openbsd.org/ssh-keygen
- RFC 4251, The Secure Shell (SSH) Protocol Architecture, section 4.1 (Host Keys): https://www.rfc-editor.org/rfc/rfc4251
- OpenSSH release notes, including 10.6 (PubkeyAuthOptions max-pk-ok): https://www.openssh.com/releasenotes.html
- OpenSSH source, sshconnect.c (host key checking messages): https://github.com/openssh/openssh-portable/blob/master/sshconnect.c
- OpenSSH source, authfile.c (private key permission check): https://github.com/openssh/openssh-portable/blob/master/authfile.c
- Debian rsyslog configuration (auth.log routing): https://sources.debian.org/src/rsyslog/8.2504.0-1+deb13u2/debian/rsyslog.conf/
- Debian openssh, ssh.service unit: https://sources.debian.org/src/openssh/1:10.6p1-1/debian/systemd/ssh.service/
- Ubuntu openssh-server packaging (ssh.service): https://git.launchpad.net/ubuntu/+source/openssh/plain/debian/openssh-server.install?h=ubuntu/noble
- Fedora openssh spec (sshd.service): https://src.fedoraproject.org/rpms/openssh/blob/rawhide/f/openssh.spec
- restorecon(8): https://man7.org/linux/man-pages/man8/restorecon.8.html
- SELinux reference policy, ssh.fc: https://github.com/SELinuxProject/refpolicy/blob/master/policy/modules/services/ssh.fc