Every check, and the mask it uses
This is the table this page exists for. Advice about SSH permissions contradicts itself
across the internet because people generalise one check to all four, and the four do not
agree. Read the mask column: the private key uses 077, everything else uses
022. All of them were read from the OpenSSH source on 2026-08-15.
| What is checked | Side | Mask | Condition in the source | Modes that pass | Recommended |
|---|---|---|---|---|---|
private key (~/.ssh/id_ed25519, id_rsa …) the key is ignored, with a three-line banner and “This private key will be ignored.” | client | 077 | (st.st_uid == getuid()) && (st.st_mode & 077) != 0 authfile.c · sshkey_perm_ok() | 8 of 512 | 600 rw------- |
client config (~/.ssh/config) ssh exits immediately with “Bad owner or permissions on …” — the connection is never attempted | client | 022 | (sb.st_uid != 0 && sb.st_uid != getuid()) || (sb.st_mode & 022) != 0 readconf.c · read_config_file_depth(), SSHCONF_CHECKPERM branch | 128 of 512 | 600 rw------- |
authorized_keys on the server the key file is skipped; the log says “Authentication refused: bad ownership or modes for file …” | server | 022 | (!platform_sys_dir_uid(stp->st_uid) && stp->st_uid != uid) || (stp->st_mode & 022) != 0 misc.c · safe_path() | 128 of 512 | 600 rw------- |
~/.ssh and every directory above it, up to $HOME the same refusal, naming the directory instead of the file | server | 022 | stat(buf, &st) == -1 || (!platform_sys_dir_uid(st.st_uid) && st.st_uid != uid) || (st.st_mode & 022) != 0 misc.c · safe_path() | 128 of 512 | 700 rwx------ |
The private-key row is the outlier and the reason "just chmod 600 everything" happens to
work: only 8 of 512 modes satisfy the
077 mask, so the advice is almost forced. But applying that same strictness
to the directory chain sends you chmod-ing parents that were never failing, while the
actual offender — a group-writable home directory — stays untouched.
Where each recommendation comes from
The enforced conditions above are code. The recommended modes are prose from
ssh(1)'s FILES section, and it is worth reading the exact wording, because
the manual is careful in a way that summaries are not:
- private key (~/.ssh/id_ed25519, id_rsa …) — ssh(1): “These files contain sensitive data and should be readable by the user but not accessible by others.”
- client config (~/.ssh/config) — ssh(1): “Because of the potential for abuse, this file must have strict permissions: read/write for the user, and not writable by others.”
- authorized_keys on the server — ssh(1): “This file is not highly sensitive, but the recommended permissions are read/write for the user, and not accessible by others.”
- ~/.ssh and every directory above it, up to $HOME — ssh(1): “the recommended permissions are read/write/execute for the user, and not accessible by others.”
Notice the phrase used for the private key: "ssh will simply ignore a private key file if it is accessible by others". Ignore, not error — which is why a botched key mode often shows up as a password prompt where you expected none, and never as an outright failure.
StrictModes, and what turning it off would mean
The server-side checks run under StrictModes, which
sshd_config(5) documents as: "Specifies whether sshd should check file modes
and ownership of the user's files and home directory before accepting login. This is
normally desirable because novices sometimes accidentally leave their directory or files
world-writable. The default is yes."
You can set it to no and your key will be accepted from a world-writable
directory. You should not: the check exists because a writable ~/.ssh means
anyone who can write there can append their own key to authorized_keys and
become you. Fix the mode, not the check. The one exception documented in the same entry is
that ChrootDirectory permissions are validated unconditionally regardless of
this setting.
The messages, and where to read them
Client-side failures print on your terminal. Server-side failures do not — the client sees
only Permission denied (publickey), identical to what it prints for a wrong
key. The specific sentence lives in the server's log:
| What you see | Where it comes from | Fires on | Fix |
|---|---|---|---|
Permissions 0644 for '/home/deploy/.ssh/id_ed25519' are too open. OpenSSH client (ssh, scp, sftp, git over ssh) · stderr, before the connection is attempted shown for mode 644 on ~/.ssh/id_ed25519 (a private key)
verbatim format string: | OpenSSH (portable) — authfile.c · sshkey_perm_ok() | (st.st_uid == getuid()) && (st.st_mode & 077) != 0 quoted verbatim from the source 4032 of the 4096 modes fire it. | chmod 600 /home/deploy/.ssh/id_ed25519 least-privilege mode 600 |
Bad owner or permissions on /home/deploy/.ssh/config OpenSSH client (ssh) · stderr; ssh exits immediately (fatal) shown for mode 664 on ~/.ssh/config verbatim format string: | OpenSSH (portable) — readconf.c · read_config_file_depth(), SSHCONF_CHECKPERM branch | ((sb.st_uid != 0 && sb.st_uid != getuid()) || (sb.st_mode & 022) != 0) quoted verbatim from the source 3072 of the 4096 modes fire it. | chmod 600 /home/deploy/.ssh/config least-privilege mode 600 |
Authentication refused: bad ownership or modes for file /home/deploy/.ssh/authorized_keys OpenSSH server (sshd) · the server’s log — /var/log/auth.log or journalctl -u sshd shown for mode 664 on ~/.ssh/authorized_keys on the server verbatim format string: | OpenSSH (portable) — misc.c · safe_path() OpenSSH (portable) — auth2-pubkeyfile.c · auth_openfile() safe_path() in misc.c writes this into an error buffer; auth_openfile() in auth2-pubkeyfile.c prints the buffer with logit("Authentication refused: %s", line). | (!platform_sys_dir_uid(stp->st_uid) && stp->st_uid != uid) || (stp->st_mode & 022) != 0 quoted verbatim from the source 3072 of the 4096 modes fire it. | chmod 600 /home/deploy/.ssh/authorized_keys least-privilege mode 600 |
Authentication refused: bad ownership or modes for directory /home/deploy/.ssh OpenSSH server (sshd) · the server’s log — /var/log/auth.log or journalctl -u sshd shown for mode 775 on ~/.ssh, and every directory above it up to the home directory verbatim format string: | OpenSSH (portable) — misc.c · safe_path() OpenSSH (portable) — auth2-pubkeyfile.c · auth_openfile() Same two-file assembly as the file variant: safe_path() fills the buffer while walking upward from the key file, auth_openfile() prefixes "Authentication refused: ". | stat(buf, &st) == -1 || (!platform_sys_dir_uid(st.st_uid) && st.st_uid != uid) || (st.st_mode & 022) != 0 — inside the loop that walks each parent in turn quoted verbatim from the source 3072 of the 4096 modes fire it. | chmod 700 /home/deploy/.ssh least-privilege mode 700 |
ssh-privkey-too-open — The check only runs when the key is owned by the user running ssh. A key owned by somebody else is not mode-checked at all — the comment in the source says so explicitly. Note also that the printed number is st_mode & 0777, so a set-user-ID bit on the key never appears in the message.
ssh-config-bad-owner — This one is fatal: ssh stops on the spot instead of ignoring the file and carrying on.
sshd-authkeys-file — The client sees only "Permission denied (publickey)". The sentence above exists only in the server’s log, which is why this failure is so often misdiagnosed as a wrong key.
sshd-authkeys-dir — The mask is 022, not 077. sshd does not require ~/.ssh to be 700 — 755 passes this test. 700 is the mode ssh(1) recommends, which is a different statement from the one sshd enforces.
The commands
On the client, for a key that is refused:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519.pub On the server, when the log says a directory is at fault, find it before changing anything:
namei -l ~/.ssh/authorized_keys
That prints the mode and owner of every component from / downward. Look for
the one with a w in the group or other position — that is the component
safe_path() stopped at. Fix that one. The rest were fine.
| Where | Mode | Basis | The rule, and why |
|---|---|---|---|
~/.ssh (the directory) directory | 700 rwx------ | documented | ssh(1) FILES: "There is no general requirement to keep the entire contents of this directory secret, but the recommended permissions are read/write/execute for the user, and not accessible by others." What sshd actually enforces is weaker: safe_path() rejects the directory only when (st_mode & 022) is non-zero, so 755 passes the login check. 700 is the documented recommendation, not the enforced floor — a distinction most cheat-sheets collapse. |
~/.ssh/id_ed25519, id_rsa (private keys) file | 600 rw------- | enforced | (st.st_uid == getuid()) && (st.st_mode & 077) != 0 → the key is ignored The mask is 077, so every bit outside the owner triad fails, read included. Exactly 8 of the 512 three-digit modes satisfy it, and 600 is the only one of those that is both readable and writable by its owner without granting execute. (200 is writable without execute too, but you cannot read the key back.) |
~/.ssh/authorized_keys (on the server) file | 600 rw------- | documented | ssh(1) FILES: "This file is not highly sensitive, but the recommended permissions are read/write for the user, and not accessible by others." Enforced floor: (st_mode & 022) == 0 when StrictModes is on. Public keys are public, so readability is not the risk; writability is. Anyone who can append a line to this file owns the account. |
~/.ssh/config file | 600 rw------- | enforced | (sb.st_mode & 022) != 0 → fatal("Bad owner or permissions on %s") ssh exits here; it does not warn and carry on. A writable client config lets somebody else add a ProxyCommand and route your sessions through a machine of their choosing. |
- enforced the program refuses to proceed otherwise — the condition is quoted from its source
- documented the project’s own manual recommends this value