Understanding File Permissions

Every file and directory on a Linux system carries a set of permissions that decide who can read it, write to it, or execute it. Understanding these permissions is essential for keeping your system secure, letting your scripts run, and sharing files safely with other users or services. This lesson explains exactly how the permission model works under the hood, how to read and change permissions with ls -l and chmod, and the mistakes that trip up almost everyone at some point.

Overview: How Linux Permissions Work

Linux is a multi-user operating system: many processes, sometimes belonging to different people, run on the same machine and share the same filesystem. To keep one user’s files safe from another user, the kernel attaches an owner, a group, and a set of permission bits to every file and directory. These are stored in the file’s inode — the on-disk structure that holds metadata about a file (its size, timestamps, owner, permissions, and pointers to its data blocks) separately from the file’s actual content.

Permissions are organized into three classes and three actions:

  • Classes: owner (the user who owns the file, often called “user”), group (a group of users who share access), and other (everyone else on the system).
  • Actions: read (r), write (w), and execute (x).

That gives nine permission bits total — three classes times three actions — and each bit is either set or unset. Read, write, and execute mean slightly different things depending on whether the target is a regular file or a directory:

Permission Effect on a file Effect on a directory
r Read the file’s contents List the names of entries in the directory
w Modify or truncate the file’s contents Create, delete, or rename entries inside the directory
x Execute the file as a program or script Enter the directory (cd into it) and access entries by name

That last row surprises a lot of beginners: a directory’s execute bit has nothing to do with “running” a directory. It controls whether you can traverse into it at all. A directory with r but no x lets you list filenames but not open or stat any of them — you need both to use a directory normally.

Octal notation

Each set of three bits (rwx) can be written as a single octal digit by adding the values r=4, w=2, x=1. A file that’s fully open to its owner (rwx = 4+2+1 = 7), readable and executable by its group (r-x = 4+0+1 = 5), and readable and executable by everyone else (r-x = 5) is written 755. Three digits — owner, group, other — describe the entire permission set in one compact number.

Special permission bits

Beyond the standard nine bits, Linux supports three special bits, added as a fourth leading octal digit: setuid (4000) makes an executable run with its owner’s privileges instead of the invoking user’s; setgid (2000) does the same for the group, and on a directory makes new files inherit that directory’s group; and the sticky bit (1000), seen on directories like /tmp, which stops users from deleting files they don’t own even if they have write access to the directory. These are advanced tools you’ll rarely set yourself, but recognizing an s or t in a permission listing (for example rwsr-xr-x) tells you one is active.

Syntax

Reading permissions with ls -l:

ls -l <file-or-directory>

Each line of output breaks down like this:

-rwxr-xr-x 1 alice devteam 312 Aug  4 10:00 backup.sh
  • Column 1 (-rwxr-xr-x): file type plus the nine permission bits. The first character is the type: - regular file, d directory, l symbolic link, and a few rarer types (c character device, b block device, p pipe, s socket). The next nine characters are owner/group/other, three each.
  • Column 2 (1): link count — how many hard links point to this inode.
  • Column 3 (alice): the owning user.
  • Column 4 (devteam): the owning group.
  • Column 5 (312): size in bytes.
  • Column 6: last modification timestamp.
  • Column 7: the filename.

Changing permissions with chmod:

chmod <mode> <file>

<mode> can be given two ways:

  • Octal: three (or four) digits, e.g. chmod 644 file — set the permissions to exactly this value, discarding whatever was there before.
  • Symbolic: chmod u=rwx,g=rx,o=rx file or incremental forms like chmod g+w file (add write for group) and chmod o-r file (remove read for other). Symbolic mode targets are u (owner), g (group), o (other), and a (all three); operators are + (add), - (remove), and = (set exactly).

Add -R to either form to apply recursively to a directory tree.

Examples

Example 1: Reading a file’s permissions

ls -l /var/log/app.log

Output:

-rw-r--r-- 1 root adm 15234 Aug  4 09:12 /var/log/app.log

The owner (root) can read and write the file; the group (adm) and everyone else can only read it. Nobody has execute permission, which makes sense — this is a log file, not a program.

Example 2: Making a script executable

ls -l backup.sh
chmod +x backup.sh
ls -l backup.sh
./backup.sh

Output:

-rw-r--r-- 1 alice alice 312 Aug  4 10:00 backup.sh
-rwxr-xr-x 1 alice alice 312 Aug  4 10:00 backup.sh
Backup completed successfully.

The freshly created script has no execute bit, so it can only be opened as text. chmod +x adds execute permission for owner, group, and other alike (a shorthand equivalent to a+x). Once the execute bit is set, ./backup.sh works: the leading ./ is required because the current directory usually isn’t in your $PATH, so bash won’t find the script by name alone.

Example 3: Locking down a sensitive file

chmod 640 ~/projects/secrets.env
ls -l ~/projects/secrets.env

Output:

-rw-r----- 1 alice devteam 128 Aug  4 10:15 secrets.env

640 breaks down as owner rw- (6), group r-- (4), other --- (0). Alice can edit the file, her devteam group can read it, and everyone else on the machine is denied access entirely — a sensible mode for a config file that might contain credentials but still needs to be readable by trusted teammates.

Example 4: Changing one class without disturbing the rest

ls -l /var/www/html/index.php
chmod g+w /var/www/html/index.php
ls -l /var/www/html/index.php

Output:

-rw-r--r-- 1 www-data www-data 4021 Aug  4 08:00 index.php
-rw-rw-r-- 1 www-data www-data 4021 Aug  4 08:00 index.php

Symbolic mode is useful precisely because it’s incremental: g+w only touches the group’s write bit, leaving owner and other exactly as they were. If you’d used chmod 664 instead you’d get the same result here, but symbolic mode is safer when you don’t want to recompute the whole octal value from scratch.

How It Works Step by Step

When a process tries to open, write to, or execute a file, the kernel performs a permission check before doing anything else:

  1. The kernel compares the requesting process’s effective user ID (EUID) to the file’s owner UID. If they match, the kernel uses the owner permission bits only — even if the group or other bits would have allowed more access.
  2. If the EUID doesn’t match the owner, the kernel checks whether the process’s effective group ID (or any supplementary group it belongs to) matches the file’s group. If so, it uses the group bits only.
  3. If neither owner nor group matches, the kernel falls back to the other bits.
  4. The relevant action bit (r, w, or x) must be set for the requested operation, or the kernel returns an access-denied error and the operation fails.

The root user is a special case: the kernel skips these checks for almost all operations, which is why commands run with sudo can read or write files no other account can touch. The one exception is execute permission — even root generally needs at least one execute bit set on a file to run it as a program, though root can still open and read its bytes regardless of the mode.

When you run chmod, no data in the file itself changes — only the mode field stored in the inode is updated. This is why changing permissions on a huge file is just as fast as on an empty one; the operation never touches the file’s actual content.

Common Mistakes

Mistake 1: Reaching for chmod 777

chmod 777 ~/projects

This grants read, write, and execute to every account on the system, not just you. On a shared or internet-facing machine that means any other user, or any process running as another user (including a compromised service), can modify or delete your files. Grant only what’s actually needed:

chmod 750 ~/projects

Here the owner keeps full access, the group can read and enter the directory, and everyone else is locked out.

Mistake 2: Forgetting to make a script executable

$ ./backup.sh
bash: ./backup.sh: Permission denied

A newly written or downloaded script defaults to non-executable permissions. Trying to run it directly fails even though the file is perfectly valid. Set the execute bit first:

chmod +x backup.sh
./backup.sh

Mistake 3: Removing a directory’s execute bit

$ chmod 600 ~/projects
$ ls ~/projects/backup.sh
ls: cannot access '/home/alice/projects/backup.sh': Permission denied

Setting a directory to 600 strips its execute bit, which blocks traversal into it entirely — even though the file inside still has its own readable permissions, nothing inside the directory can be reached. Directories almost always need their execute bit set for anyone who should access their contents:

chmod 700 ~/projects

This keeps the directory private to its owner while restoring the ability to enter it and access files by name.

Best Practices

  • Default to the least privilege that works: 644 for regular files (owner read/write, everyone else read-only) and 755 for directories and executables (owner full access, everyone else read and traverse/execute).
  • Avoid 777 outside of throwaway test environments — it grants write access to every account on the machine.
  • Use symbolic mode (chmod g+w file) when you want to change one class without recomputing and disturbing the rest of the mode.
  • Put sensitive files — SSH keys, credential files, .env files — at 600 (owner read/write only).
  • Use groups instead of world-writable permissions to share access between trusted users: add them to a shared group and set the shared directory to 770 (or 2770 with the setgid bit so new files inherit the group automatically).
  • Always chmod +x a script before running it as ./script.sh; running it via bash script.sh works without the execute bit, but then bash ignores the script’s shebang line.
  • Run ls -l after any chmod you’re unsure about instead of assuming the command did what you meant.

Practice Exercises

  1. Create a script called deploy.sh containing a single line that echoes a message. Try running it with ./deploy.sh before setting any permissions, note the error, then fix it so it runs successfully.
  2. You have a directory ~/shared-notes that must be readable and writable by you and your group, but completely inaccessible to every other user on the system. Work out the exact octal chmod command for the directory itself.
  3. Given a file with permissions -rw-r--r-- owned by user root and group root, and you are logged in as a non-root user who is not a member of the root group, determine which permission class applies to you and what you can and cannot do to the file.

Summary

  • Every file has three permission classes — owner, group, other — each with independent read, write, and execute bits.
  • ls -l displays permissions as a 10-character string; chmod changes them, in octal (755) or symbolic (u+x) form.
  • On directories, x controls traversal and r controls listing entries — they are not interchangeable, and both are usually needed together.
  • The kernel checks owner, then group, then other, stopping at the first match — so owner bits govern access even when group bits would be more permissive.
  • Root bypasses read/write permission checks but still generally needs execute permission set to run a file as a program.
  • Avoid 777; prefer the least-permissive mode that gets the job done, and use groups for shared access instead of opening files to everyone.