File Permissions: rwx, chmod, and How Access Is Actually Decided
-rw-r----- tells you something about a file, but it does not by itself answer "can this program read this file right now?" The kernel first walks every directory in the path, compares the calling process's identity against the object's owner and group, and checks the specific bit the requested operation needs. After that, an ACL, a mount option, or a security module can still change the outcome.
This article builds on the account model from User and Group Accounts: permissions are checked against a process's UID and GID, and those come from the account that started it. Here you will learn to read the rwx notation and set it with chmod; see how r, w, and x mean something different on a directory than on a regular file; understand how the kernel walks a path and picks one permission class; read id, stat, namei, findmnt, and getfacl together; and run a read-only audit with find -perm to track down a Permission denied on a service account, using the least amount of change necessary.
Reading and Setting the Permission String
ls -l shows ten characters at the start of each line, for example -rw-r-----. The first character is the object's type (- for a regular file, d for a directory, l for a symbolic link, and so on — covered in the filesystem module). The remaining nine characters are three groups of three: owner, group, and other, each showing r, w, x, or -.
mkdir -p ~/lab/file-permissions
printf 'sample line\n' > ~/lab/file-permissions/report.txt
ls -l ~/lab/file-permissions/report.txt
stat -c 'symbolic=%A octal=%a owner=%U group=%G path=%n' ~/lab/file-permissions/report.txt
A representative result:
-rw-r--r-- 1 ali ali 12 Jul 29 10:00 /home/ali/lab/file-permissions/report.txt
symbolic=-rw-r--r-- octal=644 owner=ali group=ali path=/home/ali/lab/file-permissions/report.txt
The same permission set has two common notations:
- Octal: three (or four) digits, one per class, where
r=4,w=2,x=1and the digit is their sum.644means ownerrw-(6), groupr--(4), otherr--(4). - Symbolic: the
rwxletters directly, optionally combined withchmod'su/g/o/aclass letters and+/-/=operators.
chmod: Changing the Bits
chmod 640 ~/lab/file-permissions/report.txt
chmod u=rw,g=r,o= ~/lab/file-permissions/report.txt
chmod go-w ~/lab/file-permissions/report.txt
stat -c '%A %a %n' ~/lab/file-permissions/report.txt
chmod 640 filesets the exact octal mode.u=rw,g=r,o=sets each class explicitly, clearing anything not listed.g+w,o-r,a+xadd or remove a single bit from a class without touching the others.
Symbolic mode is often safer for a small, targeted change, because it does not require you to compute the whole octal number; a typo in an octal digit changes every bit in that class at once.
Warning
chmod -R applies to every object under a directory and has no --dry-run option. Before running it against anything beyond a lab directory, preview the target with a read-only find first, and know the current mode so you can reverse the change. Never use chmod -R casually against /etc, /var, /srv, or another real service directory.
Permission Decisions Are Not a Single Line
When a user types cat /srv/app/config.ini, the cat program does not simply "see" the file. It asks the kernel to open the path, and the kernel works through a simplified decision chain:
- an absolute path starts from
/, a relative path starts from the process's current directory; - the kernel walks the path one intermediate directory at a time, requiring
x(search permission) on each one; - once the final object is found, the process is classified into exactly one triplet:
uif it owns the object,gif it belongs to the object's group, otherwiseo; - only the single bit needed for the requested operation is checked — read, write, or execute/search;
- additional rules — ACLs, a read-only or
noexecmount, Linux capabilities, or a security module — can still restrict the result; - if every check passes, the operation proceeds; otherwise the call typically fails with
EACCES, "Permission denied."
An important detail: the kernel does not add the u, g, and o permissions together, and it does not search for the most generous of the three. If the process owns the file, only the owner triplet applies — even if other happens to be more permissive than owner, the owning process cannot "switch" to the other class to get broader access.
The Process's Identity Is What Matters
A file is accessed by a process, not directly by a person. An interactive shell usually inherits the logged-in user's identity; a service can run under the account configured in its unit or daemon settings.
For the current shell:
For a specific service process:
Replace <service_name> with a real unit, such as nginx.service, and <process_name> with the executable's name. An empty User= in systemctl show often means the service runs as root by default — check the actual running process with ps instead of assuming.
A practical rule: when a permission error appears, reproduce it as the account actually performing the operation, not as the account typing in the terminal.
Directory Bits Behave Differently, in Practice
On a regular file, r, w, and x mean reading content, modifying it, and running the file as a program. A directory instead stores names and links to inodes, so its bits control different operations:
r: list the names recorded in the directory.x: look up a specific name, retrieve its metadata, and continue past it along a path.w: combined withx, create, remove, or rename a name.
A safe lab shows this directly:
mkdir -p ~/lab/file-permissions/vault
printf '%s\n' 'lab-2026' > ~/lab/file-permissions/vault/report.txt
printf '%s\n' '#!/usr/bin/env bash' 'echo "script ran"' > ~/lab/file-permissions/check.sh
chmod 0755 ~/lab/file-permissions
chmod 0700 ~/lab/file-permissions/vault
chmod 0644 ~/lab/file-permissions/vault/report.txt
chmod 0644 ~/lab/file-permissions/check.sh
Only r: Names Are Visible, the Object Is Not
chmod 0400 ~/lab/file-permissions/vault
ls ~/lab/file-permissions/vault
stat ~/lab/file-permissions/vault/report.txt
cat ~/lab/file-permissions/vault/report.txt
chmod 0700 ~/lab/file-permissions/vault
ls shows report.txt because the directory has r. stat and cat fail with something like:
report.txt
stat: cannot statx '/home/ali/lab/file-permissions/vault/report.txt': Permission denied
cat: /home/ali/lab/file-permissions/vault/report.txt: Permission denied
The cause is not the file's own 0644 — the kernel needs x on vault to look up report.txt inside it.
Only x: a Known Name Opens, Listing Does Not
chmod 0100 ~/lab/file-permissions/vault
ls ~/lab/file-permissions/vault
cat ~/lab/file-permissions/vault/report.txt
chmod 0700 ~/lab/file-permissions/vault
This time ls cannot read the directory:
But because the name was already known and the directory has x, cat reaches the file. The file's own r then allows reading its content.
w and x: Names Can Change, Listing Is Not Required
chmod 0300 ~/lab/file-permissions/vault
printf '%s\n' 'temporary' > ~/lab/file-permissions/vault/temp.txt
rm -- ~/lab/file-permissions/vault/temp.txt
ls ~/lab/file-permissions/vault
chmod 0700 ~/lab/file-permissions/vault
Creating a new name and removing a known one both work, but ls cannot list the result without r. This is not a quirk — it follows directly from the fact that directory bits govern separate, independent operations.
Warning
If the terminal disconnects partway through this lab, restore the starting state with chmod 0700 ~/lab/file-permissions/vault. Do not try these commands against /etc, /var, /srv, or any real service directory.
One Operation Can Depend on More Than One Object
"Working with a file" hides several different system calls and permission checks:
| Operation | Core requirement |
|---|---|
| read a file's content | x on every directory along the path, r on the final file |
| write to an existing file | x along the path, w on the final file; the mount must also be writable |
| create a new file | w and x on the parent directory |
| rename or delete a file | w and x on the relevant parent directory; a sticky bit can add a further restriction |
| list a directory's contents | r on the directory; x too, for full metadata |
| execute a regular file | x along the path, x on the file itself, plus a matching format/interpreter and an executable mount |
An Unreadable File Can Still Be Deleted
Deleting does not touch a file's content — it removes a name from the parent directory. This is safe to see in the lab:
printf '%s\n' 'unreadable' > ~/lab/file-permissions/delete-me.txt
chmod 0000 ~/lab/file-permissions/delete-me.txt
cat ~/lab/file-permissions/delete-me.txt
ls -ld ~/lab/file-permissions
rm -i -- ~/lab/file-permissions/delete-me.txt
test ! -e ~/lab/file-permissions/delete-me.txt
echo $?
cat is refused, but because the parent directory's owner has w and x, rm succeeds once you confirm the interactive prompt. The final exit status 0 confirms the file is gone. A sticky bit on a shared directory can narrow this rule — that mechanism belongs to the next topic on special permissions.
An x Bit Does Not Guarantee a File Runs
Add the execute bit to the lab script:
chmod u+x ~/lab/file-permissions/check.sh
stat -c '%A %a %n' ~/lab/file-permissions/check.sh
~/lab/file-permissions/check.sh
Expected result:
Here, the execute bit only cleared the permission layer. The script's shebang line, /usr/bin/env bash, also has to exist and be runnable. A binary needs a matching architecture and format, its dynamic libraries need to be found, and the file system must not be mounted noexec. So x is one of several necessary conditions for execution, not a complete guarantee.
An Open File and a Path Are Not the Same Thing
Once a program successfully opens a file, the kernel returns a file descriptor. Later read or write calls go through that descriptor, not through a fresh lookup of the name. Even if the file is renamed or unlinked afterward, the descriptor can keep referring to the same underlying object.
Because of that, do not assume that removing a permission immediately cuts off an already-open connection. If sensitive data may be leaking, do not stop at changing the mode bits — find the processes still holding the file open with lsof <path>, and restart them safely according to your organization's incident process.
Tools: Each One Answers a Different Question
stat: the Final Object's Metadata
A representative result:
mode=-rw-r--r-- (644) owner=ali(1000) group=ali(1000) path=/home/ali/lab/file-permissions/vault/report.txt
%A is the symbolic mode, %a the octal mode; %U/%u are the owner's name and UID; %G/%g the group's name and GID. If a name cannot be resolved, some fields may show the raw number instead.
namei -l: Every Component of the Path
namei -l prints the type, owner, group, and mode of every component from the root down to the file. If any directory along the path is missing the required class's x, the check stops right there. namei may be absent from a minimal image; on Ubuntu and the RHEL family it is normally part of the util-linux package.
getfacl: Rules Beyond ls -l
A + at the end of an ls -l line signals that the object may carry an extended access control list. getfacl shows named-user and named-group rules too. ACLs build on top of the classic triplet; their mask and default-ACL mechanics are covered in a dedicated ACL topic. If the command is missing on Ubuntu, it is provided by the acl package.
findmnt: Checking the File System Layer
A ro mount blocks writing; noexec blocks running a program from that file system. In either case, loosening the rwx bits does not solve the problem. Changing mount policy is a larger system operation — first determine why it was set that way, and follow the storage module's verified procedure.
The Real Operation Is the Final Proof
test -r, find -readable, and similar tools give a useful signal, but the state can change between the check and the real access, or a remote file system can decide differently. The most reliable diagnostic is a safe, side-effect-free real operation, run as the exact account that matters:
sudo here is not being used to gain privilege — it switches from an administrator session down to the service account for the purpose of testing. If the file's content should not appear on the terminal:
0 means the file could be opened for reading; a non-zero status indicates an error. This test never prints the file's content.
Auditing Permissions with find -perm
find -perm does not test whether the current user could actually open a file — it compares the mode bits stored in the inode. ACLs, a read-only mount, and a security module never enter into it. Knowing that limitation, it is still a convenient way to find suspicious modes across a large tree.
There are three ways to match:
-perm 0644: matches only if the plain permission bits are exactly0644.-perm -0640: matches if all of the listed bits are present; extra bits are ignored.-perm /0022: matches if any one of the listed bits is present; the/form is a GNUfindextension.
Auditing the lab, read-only:
find ~/lab/file-permissions -xdev -type f -perm 0644 \
-printf 'exact-0644 %m %u:%g %p\n'
find ~/lab/file-permissions -xdev -type f -perm /0022 \
-printf 'group-or-other-writable %m %u:%g %p\n'
find ~/lab/file-permissions -xdev -type f -perm /0111 \
-printf 'any-execute-bit %m %u:%g %p\n'
0022 matches a file with the group write bit (0020) or the other write bit (0002), and either is enough — they do not both need to be present. 0111 matches any file with an execute bit in any of the three classes. -xdev keeps the search within one file system.
Warning
Do not turn an audit into a mass fix by adding find ... -exec chmod .... Compare the results against the file's owner, the service's actual requirements, ACLs, and deployment policy first. A recursive change needs a snapshot or an ACL/mode backup, a test copy, and a rollback plan in advance.
A useful read-only example for production:
sudo is needed here so find can enter directories it would otherwise be blocked from auditing. Replace <app> with a verified directory name. Not every group-writable result in the output is a mistake — it may be deliberate, for a shared upload directory or runtime data. The audit's job is to surface unexpected exceptions, not to change things without context.
Practical Scenario: A Service Can See a File but Cannot Read It
Suppose a web service runs as www-data and needs to read /srv/site/releases/current/index.html. The file is 0640, owned by group www-data, yet the log shows Permission denied.
1. Check the Account and the Path, Read-Only
systemctl show -p User -p Group <web_service>
id www-data
namei -l /srv/site/releases/current/index.html
stat -c '%A %a %U:%G %n' /srv/site/releases/current/index.html
findmnt -T /srv/site/releases/current/index.html -o TARGET,FSTYPE,OPTIONS
A representative, relevant excerpt:
drwxr-xr-x root root /
drwxr-xr-x root root srv
drwxr-x--- root www-data site
drwxr----- root www-data releases
drwxr-x--- root www-data current
-rw-r----- root www-data index.html
index.html is readable by its group. /srv/site and current both have group x. The problem is the releases line's r--: names can be listed, but there is no x to look up the following current component.
2. Determine the Intended Policy
www-data should only read static files, never write them. So releases does not need group write, or to be opened to everyone — the only thing missing is g+x.
Save the current state before changing anything:
backup_file=$(sudo mktemp /root/site-releases.acl.before.XXXXXX)
sudo getfacl -p /srv/site/releases | sudo tee "$backup_file" >/dev/null
sudo chmod 0600 "$backup_file"
printf '%s\n' "$backup_file"
mktemp creates a unique name so no existing file is overwritten by accident. The ACL backup captures the owner, group, mode, and any extended rules. backup_file lives only in the current shell session — record the exact printed path in your rollback notes.
3. The Smallest Change, and Verification
sudo chmod g+x /srv/site/releases
stat -c '%A %a %U:%G %n' /srv/site/releases
sudo -u www-data -- sh -c 'exec 3<"$1"' sh /srv/site/releases/current/index.html
echo $?
The expected mode is 0750 (drwxr-x---), and the final status is 0. sudo chmod is needed because the directory is owned by root. This change does not give the service write access to the directory's entries.
4. Rollback
If the change has an unexpected effect:
Remove the backup according to your organization's retention policy once the service is confirmed healthy. If configuration management or a snapshot is available in production, that verified rollback path takes priority.
If rwx Looks Correct but the Error Persists
Do not stop the investigation here:
- check ACLs and the effective mask with
getfacl -p <path>; - check for
roornoexecwithfindmnt -T <path>; - on Ubuntu, look for AppArmor denials with
journalctl -k; - on the RHEL family, check the SELinux context with
ls -Z <path>and the audit log; - on NFS or FUSE, account for server-side UID mapping and export policy.
Disabling one of these layers just to make the error go away without a diagnosis is not a safe fix. The relationship between classic DAC and these additional layers is covered in more depth in Permission Security.
Common Mistakes
Looking Only at the Final File
stat file can look correct while a missing x somewhere in a parent directory blocks the whole path. Make namei -l one of the first checks you run.
Treating a Directory's r as Access
r enumerates names; x looks one up. Real-world directories often carry both bits together, which is why the difference is easy to miss.
Tying Deletion to a File's Own w
Removing a name is a parent-directory operation. A file at 0444 can still be deleted if the directory has w+x — unless a sticky bit says otherwise.
Treating find -perm -0022 and /0022 as the Same
-0022 requires both the group and other write bits together. /0022 matches if either is present alone. An audit's intent usually matches the second form.
Treating test -r as a Permanent Guarantee
The file or its permissions can change between the check and the real access. Building authorization as "check first, then open" creates a race condition. A program should open the file directly under the correct account and handle the resulting error.
Assuming a Script Runs Just Because It Has x
A wrong shebang, a missing interpreter, a noexec mount, or a format/architecture mismatch can all block execution. Errors like "bad interpreter" and "Exec format error" point to a cause outside the permission string entirely.
Testing a Service's Error Under Your Own Account
Success as an administrator or as root does not prove www-data, postgres, or a dedicated application account can access the same thing. Reproduce the operation, without side effects, with sudo -u <account> -- ....
Exercises
- Set
vaultto0400,0100,0500, and0700in turn. For each, record the results ofls,stat vault/report.txt, andcat vault/report.txt, and explain which bit controlled which behavior. - Set
report.txtto0000. Confirm it can still be renamed while the parent directory is0700, then restore its original name and0644mode. - Compare
check.shwith and without its execute bit. Then point its shebang at a deliberately nonexistent interpreter path, and compare the permission error against the interpreter error. Restore the file afterward. - Compare
find -perm 0644,find -perm -0644, andfind -perm /0022across at least three test files with different modes. Prove the "exact," "all," and "any" meanings with command output. - Build a short permission report for one file you can read, using
id,namei -l,stat,getfacl -p, andfindmnt -T. Do not change any system permission.
Verification criterion: in every conclusion, show which permission class the process fell into, whether x was present on every directory in the path, and exactly which operation was being requested — not just the mode number.
When you finish, inspect the lab tree read-only first:
Only once the result shows nothing but the ~/lab/file-permissions test tree:
Warning
chmod -R changes modes throughout the tree, and rm -rI deletes files with no undo. Run these only against the lab directory created above, and only after confirming the target with find. Copy any results you want to keep elsewhere first. There is no equivalent shortcut for a production tree without a snapshot or metadata backup.
References
- Linux man-pages:
path_resolution(7): https://man7.org/linux/man-pages/man7/path_resolution.7.html - Linux man-pages:
inode(7): https://man7.org/linux/man-pages/man7/inode.7.html - Linux man-pages:
access(2): https://man7.org/linux/man-pages/man2/access.2.html - Linux man-pages:
open(2): https://man7.org/linux/man-pages/man2/open.2.html - Linux man-pages:
acl(5): https://man7.org/linux/man-pages/man5/acl.5.html - GNU Coreutils manual:
chmodinvocation: https://www.gnu.org/software/coreutils/manual/html_node/chmod-invocation.html - util-linux manual:
namei(1): https://man7.org/linux/man-pages/man1/namei.1.html - GNU Findutils manual: file mode bits: https://www.gnu.org/software/findutils/manual/html_node/find_html/Mode-Bits.html
- POSIX.1-2024: general concepts, file access permissions: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap04.html