ChmodCalc

SSH Key Permissions

OpenSSH does not apply one permission rule — it applies several, with 2 different masks, on both sides of the connection. Set a mode below and the calculator tells you which of them it fails, using the projects' own guard expressions.

chmod — file mode
whoreadwriteexecutedigit
Owner6
Group0
Others0
special bits — the fourth digit

ls -l → -rw-------

chmod 600 ~/.ssh/id_ed25519
chmod u=rw,g=,o= ~/.ssh/id_ed25519

Absolute symbolic form — identical result, and the only form that can name the setuid, setgid and sticky bits.

what 600 grants on this file

  • Ownerrw-read the contents and change the contents
  • Group---nothing
  • Others---nothing

programs that refuse this mode

  • nginx

    open() "~/.ssh/id_ed25519" failed (13: Permission denied)

    fix: chmod 644 ~/.ssh/id_ed25519

  • Git

    hint: The '~/.ssh/id_ed25519' hook was ignored because it's not set as executable.

    fix: chmod +x ~/.ssh/id_ed25519

  • Any POSIX shell (bash, dash, zsh, sh)

    ~/.ssh/id_ed25519: Permission denied (exit status 126)

    fix: chmod +x ~/.ssh/id_ed25519

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.

The "modes that pass" column is the mask evaluated across all 512 three-digit modes — counted, not estimated. Each condition is quoted whole, so it also shows the ownership test beside the mode test; only the mode half is counted here, because ownership is not something chmod can change. Where the recommended mode differs from the enforced minimum, both are given: they are different kinds of claim.
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:

All four SSH messages, each with the source file and function that prints it.
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)

  • @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  • @ WARNING: UNPROTECTED PRIVATE KEY FILE! @
  • @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
  • It is required that your private key files are NOT accessible by others.
  • This private key will be ignored.

verbatim format string: Permissions 0%3.3o for '%s' are too open.

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: Bad owner or permissions on %s

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: bad ownership or modes for file %s

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: bad ownership or modes for directory %s

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.

The SSH file set, with the basis for each value.
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.

OpenSSH (portable) — ssh.1 — FILES · misc.c

~/.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.)

OpenSSH (portable) — authfile.c · ssh.1 — FILES

~/.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.

OpenSSH (portable) — ssh.1 — FILES · misc.c

~/.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.

OpenSSH (portable) — readconf.c · ssh.1 — FILES

  • enforced the program refuses to proceed otherwise — the condition is quoted from its source
  • documented the project’s own manual recommends this value

## Questions people actually ask

What permissions should an SSH private key have?

600, and OpenSSH's actual requirement is that the group and other digits are both zero — the test is (st.st_mode & 077) != 0. That leaves 8 acceptable modes out of 512. 400 is equally acceptable and slightly safer, because you cannot overwrite the key without changing the mode first.

Why does “Permissions are too open” appear even though the file is only group-readable?

Because the mask for a private key is 077, not 022. Any bit outside the owner triad fails, read included. This is stricter than the test the server applies to authorized_keys, which only rejects write bits — the two files are protecting different things, so they use different masks. Confusing the two is where most SSH permission advice goes wrong.

Does ~/.ssh have to be 700 for key auth to work?

No. sshd rejects a directory only when it is group- or world-writable, so 755 and 750 both pass. 128 of the 512 modes satisfy that test. 700 is what ssh(1) recommends for confidentiality — a good idea, and a different claim from the one that is blocking your login.

My key works locally but the server refuses it. Where do I look?

Look in the server's log; the client's output will never tell you. The client only ever says Permission denied (publickey), which is the same message you get for a wrong key, a missing key and a bad mode. The server writes the specific reason: run journalctl -u sshd -n 50 or read /var/log/auth.log and look for the line beginning "Authentication refused".

Do the parent directories really matter?

Yes — safe_path() walks upward from the key file to the home directory and applies the same test to every component. A home directory at 775 breaks key authentication even when ~/.ssh and authorized_keys are both perfect. namei -l ~/.ssh/authorized_keys prints every component with its mode, which finds the culprit in one command.

Should public keys be 600 as well?

There is no need. A .pub file is meant to be published; 644 is fine and nothing checks it. What matters is that nobody else can write to authorized_keys or to the directory containing it, because appending a line there is equivalent to being handed the account.

elsewhere on this site

other tools we run