Why "the file is 644" is not the whole answer
A file can be perfectly readable and still unreachable. The same three letters mean something different on a directory, and most "I set the file to 644 and it still says Permission denied" tickets are really a directory problem.
What you need to know already: 4.5 (permission bits, octal, which triple applies), 4.3 (sudo -u).
On a file
r read the contents
w modify the contents
x execute it
On a directory
A directory is a file whose content is a table of name -> inode number (an inode is the record that really is the file - 4.13 explains it; here, think "the file itself"). The bits apply to that table:
x TRAVERSE: use this directory as part of a path. Required to reach, stat or
open anything inside it - even something you already know the name of.
r LIST: read the table, i.e. see the filenames.
w change the table: create, rename and DELETE entries (needs x as well).
Every combination, with what you actually see
Set up once:
$ mkdir -p /tmp/d/sub && echo x > /tmp/d/sub/file
rw- (600) - read, no x. You can read the table but not use it:
$ chmod 600 /tmp/d
$ ls /tmp/d
sub
$ cat /tmp/d/sub/file
cat: /tmp/d/sub/file: Permission denied
r-- (400) - list only. ls gets the names from the table, then cannot look up any of them, so the long listing is question marks:
$ chmod 400 /tmp/d
$ ls -l /tmp/d
ls: cannot access '/tmp/d/sub': Permission denied
total 0
d????????? ? ? ? ? ? sub
That ????????? line is diagnostic: it always means "r without x on the directory". Almost never what anyone intended.
--x (100) - traverse only. You cannot list, but a name you know works:
$ chmod 100 /tmp/d
$ ls /tmp/d
ls: cannot open directory '/tmp/d': Permission denied
$ cat /tmp/d/sub/file
x
This is the useful one. Mode 711 on a directory means "you may reach what you have been told about, but you may not browse". Home directories on shared machines are sometimes 711 so a known path inside works without everyone being able to read the list of what is there.
-wx (300) - write and traverse, no list. You can create and delete names blind. This is how old-school "drop" directories worked.
Why w on a directory is the dangerous one
Deleting a file is a change to the directory (its name table), not to the file. So write permission on a directory lets you delete files inside it that you do not own and cannot even read:
$ mkdir /tmp/drop; chgrp ops /tmp/drop; chmod 775 /tmp/drop
$ echo 'Q4 plan' > /tmp/drop/plan.txt; chgrp ops /tmp/drop/plan.txt; chmod 440 /tmp/drop/plan.txt
$ ls -ld /tmp/drop; ls -l /tmp/drop/plan.txt
drwxrwxr-x 2 learner ops 4096 Sep 22 20:00 /tmp/drop
-r--r----- 1 learner ops 8 Sep 22 20:00 /tmp/drop/plan.txt
$ sudo -u appuser rm -f /tmp/drop/plan.txt # appuser: in ops, cannot read it
$ ls /tmp/drop/plan.txt
ls: cannot access '/tmp/drop/plan.txt': No such file or directory
That is what the sticky bit fixes (4.9), and why /tmp has it.
The rule for a path
To open /a/b/c/file you need x on /, /a, /a/b and /a/b/c, and then the right permission on file. One missing x anywhere up the chain and nothing below is reachable.
namei -l PATH walks the path and shows the mode and owner at every level (-l = long, like ls -l). It is the fastest way to find the level that is wrong:
$ namei -l /srv/vault/report.txt
f: /srv/vault/report.txt
drwxr-xr-x root root /
drwxr-xr-x root root srv
drwx------ root root vault
report.txt - Permission denied
Read it top to bottom as the user in question: / - other has x, fine. srv - fine. vault - drwx------, other has nothing: stop, that is the level. namei cannot even look at report.txt, and says so.
Testing as someone else
Never reason about another user's access and stop there - try it:
sudo -u appuser cat /srv/vault/report.txt
sudo -u www-data ls /var/www/reports
sudo -u appuser test -r /path && echo readable
test -r FILE (also -w, -x) exits 0 if the user running it could read (write, execute) the file - it asks the kernel the exact question, so you do not have to work it out yourself.
What you can now do
- Explain why r without x on a directory gives question marks.
- Use 711 to make a file reachable by name but the directory unbrowsable.
- Find the blocking directory on a path with
namei -land prove it withsudo -u.