Skip to content

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=1 and the digit is their sum. 644 means owner rw- (6), group r-- (4), other r-- (4).
  • Symbolic: the rwx letters directly, optionally combined with chmod's u/g/o/a class 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 file sets the exact octal mode.
  • u=rw,g=r,o= sets each class explicitly, clearing anything not listed.
  • g+w, o-r, a+x add 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:

  1. an absolute path starts from /, a relative path starts from the process's current directory;
  2. the kernel walks the path one intermediate directory at a time, requiring x (search permission) on each one;
  3. once the final object is found, the process is classified into exactly one triplet: u if it owns the object, g if it belongs to the object's group, otherwise o;
  4. only the single bit needed for the requested operation is checked — read, write, or execute/search;
  5. additional rules — ACLs, a read-only or noexec mount, Linux capabilities, or a security module — can still restrict the result;
  6. 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:

id
id -un
id -Gn

For a specific service process:

systemctl show -p User -p Group <service_name>
ps -o pid,user,group,comm -C <process_name>

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 with x, 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:

ls: cannot open directory '/home/ali/lab/file-permissions/vault': Permission denied
lab-2026

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:

-rwxr--r-- 744 /home/ali/lab/file-permissions/check.sh
script ran

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

stat -c 'mode=%A (%a) owner=%U(%u) group=%G(%g) path=%n' \
    ~/lab/file-permissions/vault/report.txt

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 ~/lab/file-permissions/vault/report.txt

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

getfacl -p ~/lab/file-permissions/vault/report.txt

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

findmnt -T ~/lab/file-permissions/vault/report.txt -o TARGET,SOURCE,FSTYPE,OPTIONS

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 -u <service_account> -- head -n 1 <file_path>

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:

sudo -u <service_account> -- sh -c 'exec 3<"$1"' sh <file_path>
echo $?

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 exactly 0644.
  • -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 GNU find extension.

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 find /srv/<app> -xdev -type f -perm /0022 \
    -printf '%m %u:%g %p\n'

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:

sudo setfacl --restore="$backup_file"
stat -c '%A %a %U:%G %n' /srv/site/releases

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 ro or noexec with findmnt -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

  1. Set vault to 0400, 0100, 0500, and 0700 in turn. For each, record the results of ls, stat vault/report.txt, and cat vault/report.txt, and explain which bit controlled which behavior.
  2. Set report.txt to 0000. Confirm it can still be renamed while the parent directory is 0700, then restore its original name and 0644 mode.
  3. Compare check.sh with 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.
  4. Compare find -perm 0644, find -perm -0644, and find -perm /0022 across at least three test files with different modes. Prove the "exact," "all," and "any" meanings with command output.
  5. Build a short permission report for one file you can read, using id, namei -l, stat, getfacl -p, and findmnt -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:

find ~/lab/file-permissions -xdev -maxdepth 3 -printf '%y %m %p\n'

Only once the result shows nothing but the ~/lab/file-permissions test tree:

chmod -R u+rwX -- ~/lab/file-permissions
rm -rI -- ~/lab/file-permissions

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: chmod invocation: 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