Anyone who has worked with SSH private keys has run into an instruction to "set it to 600." Config files, by contrast, often get 644, and executable scripts get 755. What do these three-digit numbers actually mean, and why does the right number depend on what kind of file you're dealing with? This post starts from the mechanics of Unix-style (Mac/Linux) file permissions and works up to the design principle behind them: least privilege.
Permissions as a 2D grid of who and what
Unix-family operating systems express file access as a grid: three kinds of "who" crossed with three kinds of "what."
"Who" breaks down into the file's owner, the group the owner belongs to, and everyone else ("other"). "What" breaks down into read, write, and execute. Each cell in that 3×3 grid is either granted or not, and that's exactly what a listing like -rw-r--r-- from ls -l is showing you. Strip the leading character and the remaining nine characters are three groups of three — owner, group, other — each rendered as r/w/x when granted or - when not.
Why a single digit can represent read/write/execute
Numeric notation like chmod 600 compresses that rwx combination into a single octal digit. Read is worth 4, write is worth 2, execute is worth 1 — powers of two — and you sum whichever bits are set.
Note: powers of two are used here because each of read/write/execute is tracked as an independent bit (on or off), and any sum of a subset of {4, 2, 1} maps back to exactly one combination of bits. There's no ambiguity — for example, 6 can only mean read+write (4+2), never any other combination.
- Read and write, no execute (
rw-): 4 + 2 = 6 - Read only (
r--): 4 - Read, write, and execute (
rwx): 4 + 2 + 1 = 7 - No access at all (
---): 0
A three-digit number like 600 lines up these single digits for owner, group, and other, left to right. 600 means "owner gets read+write, group and other get nothing."
What the common numbers actually mean
Reading the numbers mentioned at the top through this lens:
| Number | owner | group | other | Typical use |
|---|---|---|---|---|
| 600 | rw- | --- | --- | Private keys, config files holding secrets |
| 644 | rw- | r-- | r-- | Ordinary config files, data safe to expose |
| 700 | rwx | --- | --- | Personal directories (e.g. ~/.ssh) |
| 755 | rwx | r-x | r-x | Executable scripts, programs anyone may run |
The pattern is consistent: the owner always gets whatever access the file actually requires (read/write, sometimes execute), and group/other get either "read-only" or "nothing at all" — never write access. Combinations that grant group or other write access (666, 777) don't show up as defaults, because that means "anyone on the system can modify this file," and there are very few legitimate reasons to want that.
The principle of least privilege, enforced as a number
The pattern above is a filesystem-level embodiment of a broader security concept: the principle of least privilege.
Note: least privilege means granting a user or process only the minimum access actually required to do its job — nothing more. The more access something has, the larger the blast radius when a mistake happens or an account gets compromised.
Requiring 600 on a private key rests on the assumption that only the key's owner has any legitimate reason to read it. On a machine shared by multiple people (a shared /tmp on a multi-user server, for instance), leaving read access open to group or other means any other user on that machine can read the key's contents. SSH client libraries — both OpenSSH and paramiko — refuse to load a key in that state. The mere possibility of being read by someone else is treated as unsafe, regardless of whether it's ever actually read.
Config files, by contrast, are commonly left at 644 (read-only for group/other) precisely because there's no harm in another user seeing their contents. The same logic explains 755 on executable scripts: anyone may run it, but only the owner should be able to modify it. These numbers aren't arbitrary convention — they're a direct encoding of "who should be able to touch this file, and how."
A real example: the group/other bitmask check in core/key_perms.py
This app's SSH key permission diagnostic (core/key_perms.py::_diagnose_posix()) encodes exactly this idea in code.
mode = stat.S_IMODE(st.st_mode)
mode_str = oct(mode)[-3:].zfill(3)
# group (070) / other (007) bits set at all -> NG
group_other_bits = mode & 0o077
ok = (group_other_bits == 0)
The key detail is that this checks a bitmask, not an exact match against 0o600. 0o077 in binary is 000 111 111 — every owner bit is zero, and every group and other bit is one. ANDing that mask against mode zeroes out the owner's bits entirely and leaves only the group and other bits. If the result is zero, it's confirmed that group and other have been granted nothing at all.
That approach accepts both 600 (rw-------) and 400 (r--------, read-only) as valid. It doesn't care what the owner's own permissions are — it only cares whether group and other have been shut out. Checking for an exact match on 0o600 instead would have rejected people running with chmod 400 (a reasonable choice for anyone who wants to make accidental overwrites impossible), which would have been a stricter check than what least privilege actually calls for — its real concern is closing off exposure to group and other, not dictating what the owner can do.
Windows has no octal permissions
Numeric notation like 600 or 644 is specific to POSIX (Unix-family systems). Windows' filesystem (NTFS) doesn't have it. Instead, Windows uses ACLs (access control lists) — a set of individually granted or denied entries, one per account.
core/key_perms.py::_diagnose_windows() inspects the output of the icacls command for grants to accounts that aren't a specific individual — BUILTIN\Users, Everyone, and similar. The mechanism looks nothing like the POSIX group/other check, but the goal is identical: no one but the key's owner should have access. It's the same principle of least privilege expressed in a different vocabulary. The implementation shape changes per OS; the underlying design principle doesn't.
How this differs from the earlier V11 post
An earlier post, "Solving 'Permissions are too open' from the inside", covered the UX design behind fixing bad permissions — a diagnose-then-confirm-then-fix flow (a Phase 1/2 hybrid), and the deliberate decision not to auto-fix silently at app startup. This post is one layer earlier than that: why 600 is required in the first place, and why it's treated as sufficient. If V11 was about the design decision of how to repair a broken permission, this post is the background knowledge behind what that number actually means.
Summary
Numbers like 600, 644, 700, and 755 assign read/write/execute permissions across owner, group, and other, then compress each row's total into a single octal digit. What underlies the specific pattern of numbers is the principle of least privilege: grant access only to whoever actually needs it, and nothing beyond that. core/key_perms.py's bitmask check — confirming that group and other have been granted literally nothing — is a concrete example of that principle enforced mechanically in code. Once the meaning of the numbers is clear, an error like "permissions too open" stops being an opaque rule to obey and becomes something you can reason about directly.
Top comments (0)