"Permission denied (publickey)." is the one line OpenSSH prints when the server has refused every key the client offered, and it says nothing about why. The real reason is almost always in one of three places: the client's verbose output, the server's authentication log, or the file permissions on the server account. This guide walks through each symptom, ranks the likely causes from most to least common, shows where to look to confirm each one, and gives the fix and the prevention. It is for Linux administrators, platform engineers and anyone who owns SSH access for people, CI runners and automation accounts. Key lifecycle and server hardening are covered in the SSH Key Management Guide and the OpenSSH Server Hardening Guide, which this page does not repeat. Behaviour was checked against the OpenSSH manual pages and source on 11 November 2026.
Key takeaways
- The client message is generic by design. The server does not tell an unauthenticated client why a key failed, so diagnose from
ssh -vvvon the client and the sshd log on the server, never from the error line alone. - Most failures are mundane. Wrong user name, the wrong key offered, a key not loaded in the agent, or a public key missing from
authorized_keysaccount for the large majority of cases. - StrictModes is the silent killer. sshd ignores
authorized_keysif the file,~/.sshor any parent directory up to the home directory is group- or world-writable, or owned by another user, and it logs "bad ownership or modes" only on the server. - Algorithm drift breaks old keys. Since OpenSSH 8.8 (released 26 September 2021) RSA signatures using SHA-1 are disabled by default, so very old clients or servers can fail with a valid key.
- Never fix it by weakening the server. Turning off
StrictModes, enabling password authentication or re-addingssh-rsaglobally converts an access fault into a security exposure. Fix the account, the key or the client.
What the error actually means
During public key authentication the client offers each candidate public key in turn. For each one the server checks whether the key is authorised for the requested account (via AuthorizedKeysFile, AuthorizedKeysCommand or a trusted certificate authority) and whether its algorithm is accepted. If the key is acceptable, the client proves possession by signing. When the list of offered keys is exhausted and the server's remaining methods are only publickey, the client prints Permission denied (publickey). The text in brackets is the list of methods the server still allows, so (publickey,password) or (publickey,keyboard-interactive) tells you the server would also accept other methods.
Three facts shape the diagnosis:
- Which keys were offered: only the client knows. Verbose output shows each identity file it found, each key it offered and the server's answer.
- Why a key was refused: only the server knows. sshd writes the reason to its log, at the default
LogLevel INFOfor permission problems and atVERBOSEorDEBUGfor more detail. - Whether the account can log in at all: controlled by
AllowUsers,DenyUsers,AllowGroups, account locks and the login shell, independent of the key.
Triage in five minutes
- Run the client verbosely.
ssh -vvv user@host(the manual allows up to three-voptions). For a specific key add-i ~/.ssh/keyfile, and add-o IdentitiesOnly=yesso the agent does not offer other keys first. - Read the identity lines. Lines such as
identity file /home/u/.ssh/id_ed25519 type -1mean the file was not found. GitHub's documentation notes that-1means SSH could not find a file to use. A key that is found shows a non-negative type and an "Offering public key" line. - Read the server reply to each offer. "Server accepts key" followed by a failure points to the private key or agent (signing failed). No acceptance means the server did not find the key authorised for that account.
- Confirm the user name. Look for "Authenticating to host:22 as 'user'". A wrong account is the single most common cause.
- Check the agent.
ssh-add -l -E sha256lists loaded key fingerprints. Compare them with the fingerprint of the key inauthorized_keys(ssh-keygen -lfon the public key). - Read the server log. On systemd hosts
journalctl -u sshorjournalctl -u sshd(the unit name varies by distribution), otherwise/var/log/auth.log(Debian, Ubuntu) or/var/log/secure(RHEL family).
Symptom and cause reference
Causes are listed from most to least common in typical estates. Server log messages are quoted from the OpenSSH portable source (auth.c, misc.c, auth2-pubkey.c); exact prefixes vary with version and syslog format.
| Likely cause | Where it shows | Evidence | Fix |
|---|---|---|---|
| 1. Wrong remote user | Client -v; server log | "Authenticating to ... as 'x'"; server logs Invalid user x from ... for unknown accounts | Use the right account (for Git hosting this is git; on cloud images the image's default user) |
| 2. Wrong or no key offered | Client -v | type -1 on identity lines; the needed fingerprint never appears in "Offering" | -i plus IdentitiesOnly yes, or an IdentityFile entry in ~/.ssh/config |
| 3. Public key not authorised | Server | Key absent from the account's authorized_keys, wrapped across lines, or carrying a from= option that excludes the client | Add the single-line public key; verify with ssh-keygen -lf |
| 4. Too many keys in the agent | Client -v; server log | Disconnect with "Too many authentication failures"; server logs "maximum authentication attempts exceeded" (MaxAuthTries defaults to 6) | IdentitiesOnly yes per host; fewer agent keys |
| 5. StrictModes rejection | Server log only | Authentication refused: bad ownership or modes for directory /home/u/.ssh (or for file) | Correct owner and remove group/other write on home, ~/.ssh and authorized_keys |
| 6. Client ignores private key | Client | WARNING: UNPROTECTED PRIVATE KEY FILE! and "bad permissions: ignore key" | chmod 600 (or 400) on the private key; owner-only ACL on Windows |
| 7. Account not allowed | Server log | User u from h not allowed because not listed in AllowUsers, "not in any group", "account is locked" or a shell problem | Adjust AllowUsers/AllowGroups through change control, unlock or fix the shell |
| 8. Algorithm not accepted | Client -vvv; server log | Key offered but skipped; server mentions PubkeyAcceptedAlgorithms | Move to Ed25519 or ECDSA; a scoped +ssh-rsa only as a temporary per-host exception |
9. Wrong AuthorizedKeysFile or command | Server config and log | File path differs from the default .ssh/authorized_keys .ssh/authorized_keys2; AuthorizedKeysCommand errors or times out | Run sshd -T to print effective config; test the command as its configured user |
| 10. Certificate problems | Server log | Expired certificate, principal not in AuthorizedPrincipalsFile, CA not in TrustedUserCAKeys | Reissue with correct principals and validity; see the SSH Protocol and Certificate Authority Guide |
| 11. Mandatory access control | Audit log | SELinux denial on a relocated or restored .ssh directory with a wrong label | Restore the default file context instead of disabling enforcement |
Diagnosing the client side
Which key is actually offered
The ssh manual lists the default identity files: ~/.ssh/id_rsa, id_ecdsa, id_ecdsa_sk, id_ed25519, id_ed25519_sk and, in current releases, id_mldsa44_ed25519. A key with any other name is never tried unless you pass -i, set IdentityFile, or load it into the agent. When an agent is running, its keys are offered first, which is how a laptop with eight keys hits MaxAuthTries before reaching the right one.
- Pin the key per host: a
Hostblock withIdentityFileandIdentitiesOnly yesis the durable fix for multi-key users and for Git hosting accounts with several identities. - Jump hosts: with agent forwarding the forwarded keys are offered, not files on the jump host; prefer
ProxyJump. - Elevated shells: GitHub warns against running Git with
sudo, because root's~/.sshand agent are different from the user's, so the key you generated is not the one offered.
Private key permissions on the client
The ssh manual states that ssh will ignore a private key file if it is accessible by others. AWS documents the resulting warning block, ending in "This private key will be ignored." and then the permission denied line. On Linux and macOS set the key to 600 or 400. On Windows, AWS documents resetting the ACL with icacls so that only the current user has read access and inheritance is removed.
Diagnosing the server side
Read the effective configuration
Run sshd -T (as root) to print the configuration sshd will actually use, including defaults and included files. Add -C user=alice,host=client.example,addr=203.0.113.10 to evaluate Match blocks for a specific connection, because a Match block can override AuthorizedKeysFile, PubkeyAuthentication or AuthenticationMethods for some users only. Run sshd -t after every edit to validate syntax before reload.
StrictModes and ownership
StrictModes defaults to yes. In the OpenSSH source the check rejects the key file if it is not a regular file, is owned by someone other than the user (or root), or is writable by group or others (mode bits 022), and then walks every parent directory up to the home directory applying the same owner and write test. That means:
- Typical triggers: a home directory created as
775by a provisioning script, a.sshdirectory copied by root and left root-owned, or a shared group-writable home on NFS. - Safe settings: home directory without group or other write,
~/.sshat700,authorized_keysat600, all owned by the account. These are stricter than the minimum the code enforces and match the ssh manual's recommendations and the AWS recovery steps. - Wrong fix: setting
StrictModes nolets any user who can write to another user's directory plant a key.
Raise log detail temporarily
sshd supports LogLevel values from QUIET to DEBUG3, default INFO. VERBOSE logs the fingerprint of each key used to log in and is a reasonable permanent setting for audit. Use DEBUG levels only briefly: they can record sensitive detail. A safer alternative is a one-off debug instance on a spare port (sshd -d -p 2222), closed afterwards.
Account and policy gates
The source logs explicit reasons when an account is blocked before keys are considered, for example "not allowed because not listed in AllowUsers", "not in any group" and "account is locked". Check passwd -S or chage -l for locks and expiry, and confirm the login shell exists and is listed in /etc/shells. If AuthenticationMethods requires publickey,keyboard-interactive or similar, a valid key is only the first step and the second factor must also succeed.
Key types, algorithms and version drift
| Situation | Behaviour | Recommended action |
|---|---|---|
| RSA key, modern client and server | Signs with rsa-sha2-256 or rsa-sha2-512; works | Plan migration to Ed25519 at next rotation |
Modern client (8.8 or later) to a legacy server that only supports ssh-rsa | RSA/SHA-1 signatures disabled by default; authentication or host key negotiation fails | Upgrade the server; a per-host PubkeyAcceptedAlgorithms +ssh-rsa in ~/.ssh/config only as a time-boxed exception, as the 8.8 release notes advise |
| DSA key | Not offered or not accepted by current OpenSSH | Replace with Ed25519 |
| Ed25519 on FIPS-constrained hosts | GitLab notes Ed25519 may fail on some FIPS systems | Use ECDSA P-256 or RSA per the host's approved algorithms |
If the organisation has narrowed PubkeyAcceptedAlgorithms for compliance, keys outside the list fail despite a valid authorized_keys entry: compare the offered key type with sshd -T | grep pubkeyacceptedalgorithms.
Automation, CI and Git hosting
Machine use of SSH fails in different ways from human use, and the fixes carry more risk because they are scripted.
- CI runners: the key is usually injected from a secret store into a file or agent at job start. Check that the file is written with
600permissions, has a trailing newline, and thatIdentitiesOnly yesis set. A key pasted with Windows line endings or a missing final line fails as "invalid format" or is silently skipped. - Deploy keys: Git hosts typically allow one repository per deploy key. Using the same key for a second repository is refused. Audit these regularly; see How to find unused GitHub deploy keys and SSH keys.
- Git user: GitHub documents that connecting as your GitHub user name rather than
gitreturns permission denied. Successfulssh -T [email protected]returns a greeting with the account name, which also tells you which account the key belongs to. - Cloud images: AWS documents per-image default users such as
ec2-user,ubuntu,admin(Debian) androcky, and that the instance's key pair name must match the key you hold. Prefer the provider's identity-based access (such as short-lived keys pushed per session) to sharing a launch key. - Service accounts: automation accounts with
authorized_keysentries are non-human identities. Add afrom=restriction and, where possible, a forcedcommand=, and record an owner for each key. See the SSH Key Management Guide.
Fixing safely: an ordered approach
- Keep a session open. Before changing
sshd_configor permissions on a remote host, keep an existing root or console session so a mistake does not lock you out. - Fix the client first. User name,
-i,IdentitiesOnly, agent contents and private key permissions need no server change. - Fix the account second. Correct ownership and modes, add the public key on a single line, confirm fingerprint match.
- Change server configuration last. Edit, run
sshd -t, reload (not restart) the service, and test with a new connection while the old session stays open. - Roll back cleanly. Keep the prior
sshd_configas a dated backup; if the new connection fails, restore it, validate and reload. - Remove temporary exceptions. Debug instances, raised log levels and per-host algorithm overrides get an expiry date in the ticket.
If access is lost entirely on a cloud VM, use the provider's serial console or documented offline volume recovery rather than opening password authentication to the internet.
Detection and measurement
Permission denied events are also the background noise of brute force and key-spraying. Distinguish the two.
- What to log: sshd at
LogLevel VERBOSEso accepted and failed public key events include key fingerprints; forward to the SIEM with host, user, source address and fingerprint fields parsed. - Signals of attack: many
Invalid userlines from one source, failures across many hosts for the same fingerprint, or "Too many authentication failures" from addresses outside your estate (ATT&CK T1110 Brute Force). - Signals of persistence: a new, unexplained line in
authorized_keyson a host or service account (ATT&CK T1098.004 SSH Authorized Keys). The Cursor marketplace extension compromise is a recent example of an attacker planting their own SSH key through a tampered developer tool.
| Metric | Why it matters | Target direction |
|---|---|---|
| Share of tickets resolved by weakening config (StrictModes off, passwords on) | Measures unsafe fixes | Zero |
Hosts with authorized_keys entries without a recorded owner | Unowned keys cannot be safely removed or fixed | Down |
| Keys using RSA/SHA-1 only or DSA | Predicts version-drift outages | Zero |
Hosts logging at VERBOSE with fingerprints in the SIEM | Coverage for both diagnosis and detection | Up to 100% |
Common failure patterns
- Disabling StrictModes to "make it work": fixes the symptom and lets any writer of the directory add keys.
- Re-enabling passwords during an incident: the outage ends and an internet-facing password login begins. Use console access instead.
- Global
+ssh-rsa: a server-wide algorithm exception added for one legacy client and never removed. - Copying keys between hosts with root: leaves
.sshroot-owned and fails StrictModes on every host it touched. - Shared launch keys: one cloud key pair used by a whole team, so "permission denied" after a rotation hits everyone and the fix is to distribute the private key again. The GitHub RSA SSH host key exposure shows how much work a single exposed private key creates.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST CSF 2.0 | PR.AA-01, PR.AA-03, PR.PS-04 | Credential management, authentication of users and services, log generation |
| NIST SP 800-53 Rev. 5 | IA-2, IA-5, AC-6, AC-7, AU-2 | Authentication, authenticator management, least privilege, failed logon handling, event logging |
| CIS Controls v8.1 | 5 Account Management, 6 Access Control Management, 8 Audit Log Management | Account inventory, key-based access, log collection |
| ISO/IEC 27001:2022 Annex A | 5.17 Authentication information, 8.5 Secure authentication, 8.15 Logging | Key handling, authentication mechanism, event records |
| MITRE ATT&CK v20 | T1021.004 SSH, T1098.004 SSH Authorized Keys, T1552.004 Private Keys, T1110 Brute Force | What the same logs reveal when the failures are malicious |
Practitioner checklist
- Always start with
ssh -vvvand the server log, not with configuration changes. - Confirm the remote user name and the fingerprint of the key actually offered.
- Pin keys per host with
IdentityFileandIdentitiesOnly yes. - Keep home without group/other write,
~/.sshat 700,authorized_keysat 600, all owned by the account. - Use
sshd -T -C ...to see effective settings, includingMatchblocks. - Validate with
sshd -t, reload, and test with a second session before closing the first. - Never resolve an access fault by disabling StrictModes or enabling password login.
- Run sshd at
LogLevel VERBOSEand alert on unexpected newauthorized_keysentries. - Retire DSA and RSA/SHA-1-only keys and time-box any algorithm exception.
Standards and references
- OpenBSD: sshd_config(5) manual page
- OpenBSD: ssh(1) manual page
- OpenSSH: Release notes for OpenSSH 8.8
- OpenSSH: openssh-portable source repository
- GitHub Docs: Error: Permission denied (publickey)
- GitLab Docs: Troubleshooting SSH
- AWS: Troubleshoot issues connecting to your EC2 Linux instance
- MITRE ATT&CK: T1098.004 SSH Authorized Keys
Related NHI Mgmt Group resources: SSH Key Management Guide, OpenSSH Server Hardening Guide, SSH Protocol and Certificate Authority Guide, How to Find ssh authorized keys across a server fleet, SSH Key Compromise Response Playbook.