Almost every Linux permissions problem reduces to two misunderstandings. The first is that a permission bit on a directory means something different from the same bit on a file. The second is that numeric modes like 755 are shorthand for a decision about who can do what, and people copy them without making the decision. This is the reference for both, plus the special bits, umask, ACLs, and the recursive flag that has ended more than one deployment.

A permission mode has two halves that people read as one. On a file, r means “read the bytes”. On a directory, r means “list the names inside” and has nothing to do with reading contents; you need x on the directory to actually reach a file, even if you already know its name. That single distinction explains most of “I can see the file but not open it”. For translating between symbolic and numeric notation, the chmod calculator does the arithmetic without you having to hold it in your head.

Key takeaways:

  • rwx means different things on a file and a directory. On a directory, x is “traverse”, not “execute”.
  • Octal is three groups of three bits: owner, group, other. Each group is read=4, write=2, execute=1.
  • The execute bit on a regular file has a second meaning: it marks the file as executable. Clearing it removes that, regardless of mode arithmetic.
  • umask does not grant permissions. It subtracts them from what you create, which is why a file created by one tool is unreadable by another.
  • chmod -R on a directory containing symlinks is where real damage happens. Use -P or think before you run it.

The two halves of a permission mode

A mode has two independent parts. The first is the special bits, which occupy a leading digit. The second is three permission groups — owner, group, other — each of which is a read, write and execute bit.

DigitSpecial bitOctal
1stsetuid4000
2ndsetgid2000
3rdsticky1000
The leading digit is usually omitted, which is why 755 and 4755 differ only in the special bits.

Then three digits, one per group, in the order owner, group, other:

BitValueLetter
read4r
write2w
execute / traverse1x
Add them up within a group. 4+2+1 = 7, 4+2 = 6, 4+1 = 5, 4 = 4, and so on. The order of the letters in symbolic notation is always rwx, regardless of how you add them.

The modes you actually see

ModeSymbolicTypical use
600rw-------SSH private keys, tokens, anything secret
644rw-r--r--Ordinary readable files, public keys, config
700rwx------Private directories such as ~/.ssh
755rwxr-xr-xDirectories, and executable scripts everyone may run
664rw-rw-r--Group-shared files in a collaborative directory
775rwxrwxr-xGroup-shared directories
640rw-r-----Files readable by owner and group only
777rwxrwxrwxAlmost always a mistake. Treat it as a bug report
The trailing digits repeat by habit. 755 is not “7 for everyone” — it is 7, 5, 5.

Why directories are different

This is the part that catches people out. On a file:

  • r — open and read the contents
  • w — modify the contents
  • x — execute it as a program

On a directory, the same three bits describe something else entirely:

  • r — list the names of the entries inside
  • w — create, delete and rename entries inside it
  • x — traverse it: use any name inside as part of a path

The consequence people trip over: to open /data/secrets/key.pem you need x on both /data and /data/secrets. Without it, you cannot reach the file even though you can see its name in the listing. This is the “I can list the directory but not read the file” case, and it is a missing execute bit, not a missing read bit.

Two more directory-specific behaviours worth internalising:

  • Deleting an entry needs w on the directory, not on the file. You do not need permission on the file itself to unlink it, only write permission on its parent directory. This is why “the file is read-only” does not stop deletion.
  • Read-only directory (r-x) is a genuine lock. You can list it and traverse it, but nothing can be created, deleted or renamed inside.

Symbolic notation

Symbolic notation is a clause grammar: [who][operator][permissions], and you can run several clauses in one command.

chmod u+x script.sh      # add execute for the owner
chmod go-w config.yaml  # remove write for group and other
chmod a=r notes.txt     # set all three groups to read only
chmod u=rw,go=r file    # owner read-write, group and other read-only
WhoMeaning
uuser, the owner
ggroup, the file’s group
oother, everyone who is neither
aall three of the above
OperatorMeaning
+add these bits
-remove these bits
=set to exactly these bits, clearing the rest
The = operator is the one that surprises people. chmod a=r removes write and execute from everyone, because = replaces rather than adds.

One special letter is worth knowing. A capital X sets the execute bit only if the target is a directory or already has some execute bit set:

chmod -R a+X directory   # make directories traversable,
                         # and only make already-executable files executable

That is the correct idiom for a recursive “make it usable” and avoids the common mistake of turning every data file into an executable one. If you have ever run chmod -R 755 over a tree of scripts and data, you have made every data file executable; capital X is how you avoid it.

umask: why your files are not what you expected

umask is a subtraction, not a grant. Whatever bits are set in the mask are removed from the permissions a new file or directory would otherwise get. It is set per shell and inherited by child processes, which is why a file created by your editor can be unreadable by the program you expected to read it.

umaskFiles becomeDirectories becomeTypical setting
022644755Default for most users
002664775Group collaboration
077600700Private, per-user only
027640750Group readable, others excluded
umask            # show the current mask
umask 077        # new files private to me
umask -S         # show it symbolically, e.g. u=rwx,g=,o=

If a file’s permissions are not what the creating tool’s documentation promised, check umask before anything else. It is almost always the cause.

The special bits

BitNumericEffect
setuid4000Executables run with the privileges of the file owner. On scripts, most kernels ignore it for security reasons
setgid2000Executables run with the owner’s group. On a directory, new files inherit the directory’s group
sticky1000On a directory, only the owner of a file — or the directory’s owner or root — may delete or rename it
chmod 4755, chmod 2755, chmod 1777.

The sticky bit is the one you interact with constantly without naming it: /tmp is normally 1777. Everyone may create files, and you may delete only your own — which is precisely the problem a shared temporary directory has.

setgid on a directory is the other quietly useful one. On a shared team directory, chmod 2775 . makes every newly created file inherit the directory’s group rather than the creator’s primary group, which removes an entire category of “why can’t my colleague write to this”.

Recursive chmod is where things break

Two hazards, both real.

  • Symlinks. chmod -R follows symlinks by default in GNU coreutils, so a link pointing at /etc or at a user’s home directory will have that target’s permissions rewritten. The recursion stops at mount points unless you ask otherwise, but symlinks are followed. Use -P (do not traverse symlinks) or -H when you know exactly what you want.
  • Losing the mode you were fixing. chmod -R 755 /some/dir does not “make things accessible”. It sets every regular file in the tree to 755, making data files executable, and it resets any carefully chosen 600 on a key file to 755. Use -R a+X or a find with an explicit type test.
# Traverse without following symlinks
chmod -R -P directory

# Only directories, not files
find directory -type d -exec chmod 755 {} +

# Only files, and only shell scripts, safely
find directory -type f -name '*.sh' -exec chmod 755 {} +

# See what is currently there before changing anything
find directory -type f -perm /600 -ls

GNU coreutils also ships a guard for the genuinely catastrophic case: --preserve-root, which refuses to operate on /. It is the default on chmod in recent coreutils, but check rather than assume — the argument order and the recursive flag have both been involved in real incidents.

Useful flags and recipes

# Copy the mode from another file rather than guessing it
chmod --reference=/path/to/reference file

# Report what changed without changing anything
chmod -v 644 config.yaml

# See the current mode before and after
stat -c '%a %A %U:%G %n' file

# Find everything group-writable, a common audit query
find . -perm /g+w -ls

chmod --reference deserves special mention as a habit. When you do not know what mode a file should have, copying the mode of a comparable file that already works is more reliable than reasoning from the octal table.

Fixing a failed SSH key

The most common permissions problem in practice, so it is worth having the answer to hand. SSH refuses private keys that are group- or world-readable:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_ed25519.pub

# Verify
stat -c '%a %n' ~/.ssh ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
# 700 .ssh
# 600 .ssh/id_ed25519
# 644 .ssh/id_ed25519.pub

Public keys are 644 because they are not secret. Private keys are 600 because a readable private key is a compromised private key.

When octal is not enough: ACLs

The three-group model cannot express “user alice can read this one file”. POSIX ACLs can, by attaching a named user entry and named group entries alongside the three groups:

getfacl report.txt
setfacl -m u:alice:r-- report.txt
setfacl -m u:bob:rwx projects/
setfacl -x u:alice report.txt      # remove an entry
setfacl -b file                     # remove all extended entries

Two things to know before you use them. First, a mask entry caps the effective permissions of all named users and named groups, so adding a named entry can appear not to work until you check the mask. Second, a default ACL on a directory is inherited by new entries, which is the ACL equivalent of umask and interacts with it.

Check whether the filesystem supports ACLs before relying on them; some do not, and the failure is silent in the sense that the entries are stored and then ignored.

The questions worth asking

  • Is this a file or a directory? That changes what every bit means.
  • Am I looking at the mode or at the umask that produced it?
  • Does the file’s group include the people I intend, and is my own primary group what I think it is?
  • Is there an ACL above the mode in play?
  • For a recursive change: does the tree contain symlinks, and does it contain files whose current mode matters?
  • Am I changing permissions, or ownership? They are different tools, and confusing them leaves the problem unsolved.

Most permission problems are one of these six. The chmod calculator handles the octal arithmetic if you want to check your reading, and if you want the surrounding problem rather than the bits, how least privilege works in practice on Linux covers choosing modes deliberately rather than by habit.

References

Related Tools

Try these companion tools on DevStackIO Tools: