Computer & OS Fundamentals · beginner · ~11 min

Files, directories, and permissions

By the end of this lesson you will be able to: - Navigate the Unix filesystem tree and tell absolute paths apart from relative paths. - Read any permission string such as `-rwxr-x---` and translate it to and from its octal form (for example `750`). - Explain what read, write, and execute mean **differently** for files versus directories. - Describe the owner / group / other model and how the kernel decides which triad applies to a given user. - Explain what the setuid, setgid, and sticky bits do, and why setuid binaries are a recurring security concern. - State the root (UID 0) exception and why dropping privileges matters for real services.

Overview

On a Unix-like system (Linux, macOS, the BSDs) almost everything is represented as a file: your documents, your programs, your configuration, even devices like the keyboard and the disk. All of these files live in a single tree that begins at the root directory, written /. This builds directly on What an operating system does: one of the OS's central jobs is to manage resources and decide who is allowed to do what. File permissions are how that decision is recorded and enforced for the filesystem.

When any program tries to open, change, or run a file, the kernel checks the file's permissions against the identity of the user running that program. If the check fails, the operation is refused with a "Permission denied" error. This single mechanism underpins multi-user systems, server security, and most day-to-day access control on Unix.

The vocabulary you will meet here:

  • Path — the address of a file in the tree (/etc/passwd).
  • Owner / group / other — three categories of user the system distinguishes.
  • rwx — the read, write, and execute permission bits.
  • Octal mode — a compact three-digit number (like 750) that encodes those bits.
  • setuid / setgid / sticky — special bits that change how a program runs or how a shared directory behaves.
  • root — the all-powerful administrative user, UID 0.

These ideas connect forward to the next lesson, Environment variables and the command line, because the shell you use to inspect and change permissions runs as you, with your identity and your limits.

Why it matters

Permissions are the most fundamental access-control model on Unix, and you will audit them again and again throughout a career in software or security.

Real incidents trace back to mis-set permissions:

  • A world-writable configuration file lets any local user rewrite what a service does.
  • A private key left at mode 644 (readable by everyone) leaks the moment another account is compromised.
  • A buggy setuid binary owned by root can be turned into a path to full system control.
  • A service that runs as root turns a small bug into a total compromise, because root bypasses every check.

Whether you are deploying a web server, hardening a laptop, or reviewing someone else's system, "who can read, write, or run this?" is one of the first questions worth answering. Getting permissions right is cheap; getting them wrong is one of the most common and most exploited mistakes in real systems.

Core concepts

1. The filesystem tree under /

Definition. Every file and directory on the system hangs off a single hierarchy whose top is the root directory /.

Directories are themselves just files; their "contents" are a list mapping names to other files. There are no separate drive letters as on Windows — additional disks are mounted at some point inside the one tree.

A path is the route from somewhere to a file:

  • Absolute path — starts at / and is unambiguous: /home/ana/notes.txt.
  • Relative path — starts from your current working directory: notes.txt, or ../logs/today.log (.. means the parent directory, . means the current one).
/                     <- root
|-- etc/
|   |-- passwd        absolute path: /etc/passwd
|-- home/
|   |-- ana/
|   |   |-- notes.txt absolute path: /home/ana/notes.txt
|   |-- bob/
|-- tmp/

When it matters / pitfall. Scripts that use relative paths break when run from a different directory. Prefer absolute paths in cron jobs, services, and anywhere the working directory is not guaranteed.

Knowledge check. If your current directory is /home/ana, what file does the relative path ../bob/notes.txt refer to?

2. Owner, group, and other

Definition. Every file records a single owner (a user) and a single group. Every other user falls into the catch-all category other.

When a process accesses a file, the kernel picks exactly one triad to test, in this order:

  1. If the process's user is the owner, the owner bits apply (and only those).
  2. Else if the process's user is in the file's group, the group bits apply.
  3. Else the other bits apply.

This "first match wins" rule surprises people: if you are the owner and the owner bits forbid an action, you are denied even if the group or other bits would have allowed it.

Process user = ana, file owned by ana (group: staff)

  is user == owner?   yes  ->  use OWNER bits, stop
                       no
  is user in group?    ...
  otherwise            ... OTHER bits

Knowledge check (predict the outcome). A file is owned by ana, mode is r--rw---- (owner r--, group rw-, other ---). ana is also a member of the file's group. Can ana write to the file?

3. The rwx bits — and what they mean for directories

Definition. Each triad has three bits:

Bit On a file On a directory
read (r) view the file's contents list the names inside
write (w) change the file's contents create, rename, or delete entries inside
execute (x) run the file as a program enter / traverse the directory (use names in it)

The directory column trips up almost everyone. Notable consequences:

  • To delete a file you need write + execute on its directory, not write on the file itself.
  • With x but not r on a directory, you can cd into it and open files whose names you already know, but you cannot list its contents.
  • With r but not x, you can see the names but cannot actually open or cd into anything.
dir 'secret'  =  --x  (execute only)
  ls secret/            -> Permission denied (no read)
  cat secret/known.txt  -> works IF you know the name (have execute)

Pitfall. Removing execute on a parent directory silently blocks access to everything beneath it, regardless of the children's own permissions.

4. Octal (numeric) modes

Definition. Each triad of three bits is one octal digit, because three bits count 0–7. Add the values that are set:

  r = 4    w = 2    x = 1

  rwx = 4+2+1 = 7
  r-x = 4+0+1 = 5
  rw- = 4+2+0 = 6
  r-- = 4+0+0 = 4
  --- = 0

So -rwxr-x--- is owner 7, group 5, other 0 = 750. Common modes you will see:

Octal Meaning Typical use
644 owner rw, group/other r regular documents
600 owner rw, nobody else private keys, secrets
755 owner rwx, group/other r-x programs, public directories
750 owner rwx, group r-x, other nothing group-only program/dir
777 everyone full access almost always a mistake

Pitfall. chmod 777 is a frequent "just make it work" fix that makes a file world-writable — anyone on the system can replace its contents. Treat any 777 as a red flag.

Knowledge check (explain in your own words). Why does octal work so neatly for permissions? (Hint: how many bits are in one triad, and how many values can that many bits represent?)

5. Special bits: setuid, setgid, sticky

Definition. Beyond the nine rwx bits there are three special bits, shown as a leading octal digit (so a full mode is four digits, e.g. 4755).

  • setuid (4000) — when set on an executable file, the program runs with the file owner's identity, not the caller's. The classic example is passwd: a normal user runs it, but it must edit the root-owned password database, so it is setuid root. Shown as s in the owner execute position: -rwsr-xr-x.
  • setgid (2000) — on an executable, runs with the file's group. On a directory, new files created inside inherit the directory's group — handy for shared project folders.
  • sticky bit (1000) — on a shared directory like /tmp, it restricts deletion: even though everyone can write there, you may only delete files you own. Shown as t: drwxrwxrwt.
-rwsr-xr-x  root root  /usr/bin/passwd
     ^
     setuid: runs AS root even when ana launches it

When to use / when NOT to. setgid directories are a good fit for team folders. setuid programs should be rare, small, and carefully written — every setuid-root binary is a potential privilege-escalation path if it has a bug. Never set setuid on a shell script (most systems ignore it precisely because it is so dangerous).

Pitfall. Adding setuid to your own helper "so it can write a system file" is a textbook way to introduce a vulnerability. Prefer a properly scoped service or sudo rule instead.

6. The root exception

Definition. The user root (UID 0) bypasses all file permission checks. root can read, write, and (with execute set anywhere) run essentially anything.

This is why gaining root is the objective of most local privilege-escalation work, and why defensive practice insists that services drop privileges and never run as root unless they genuinely must. A bug in a process running as root is a bug with root's power.

Pitfall. "It only works when I run it with sudo" is usually a permissions problem to fix, not a reason to run everything as root.

Syntax notes

Reading a long listing (ls -l) and the structure of a mode string:

-rwxr-x---  1  ana  staff  4096  Jun 27 10:12  report.sh
^|__||__||__|
|  |   |   |
|  |   |   +-- other  : ---  (nothing)
|  |   +------ group  : r-x  (read, execute)
|  +---------- owner  : rwx  (read, write, execute)
+------------- type   : -    (regular file; 'd' = directory, 'l' = symlink)

The leading character is the file type, not a permission. The nine characters after it are the three triads. A s or t replaces an x to indicate a special bit.

Common commands (you read these here; you will practice them in a real shell later):

ls -l file              # show type, permissions, owner, group
chmod 750 file          # set mode numerically (owner rwx, group r-x, other ---)
chmod g-w,o-rwx file     # symbolic: remove group write and all 'other' access
chmod u+x script.sh      # add execute for the owner
chmod 4755 prog          # set setuid + 755 (use with great caution)
chown ana:staff file     # change owner to ana, group to staff (needs privilege)
stat -c '%a %U %G' file  # print octal mode, owner, group (GNU/Linux)

Lesson

Almost everything on a Unix system is a file. Access to each file is controlled by permissions.

The filesystem tree

Files live in a single hierarchy that is rooted at /. Directories are themselves files; their contents are lists of other files.

Paths come in two forms:

  • Absolute — starts at the root, such as /etc/passwd.
  • Relative — starts from the current directory, such as ../notes.txt.

Permission bits

Each file has an owner, a group, and three permission triads: owner, group, and other. Each triad has three bits:

  • read (r) — view the contents.
  • write (w) — change the contents.
  • execute (x) — run the file as a program.
-rwxr-x---   owner=rwx   group=r-x   other=---

The bits are often written in octal (base 8). Each triad becomes one digit:

  • rwx = 7
  • r-x = 5
  • --- = 0

So the example above is 750.

For a directory, the execute bit has a special meaning: it grants permission to enter the directory.

Special bits (preview)

  • setuid / setgid. These make a program run as its owner or group, rather than as the user who started it. This is powerful, and a classic privilege-escalation vector when misused.
  • Sticky bit. On a shared directory such as /tmp, the sticky bit stops users from deleting each other's files.

The root exception

The root user (UID 0) bypasses all permission checks.

This is why gaining root is the goal of Linux privilege escalation. It is also why services should drop privileges and should never run as root unless they truly must.

Code examples

Here is a small, realistic audit snippet a learner could run safely on a local, isolated machine to find files worth a second look. It only reads metadata — it changes nothing — which is the right default for any review.

#!/usr/bin/env bash
# perm-audit.sh — read-only permission review for a directory you own.
# Usage: ./perm-audit.sh /path/to/review
# Safe by design: it only lists; it never chmod/chown/deletes anything.

set -euo pipefail                      # stop on errors and unset variables

TARGET="${1:?usage: perm-audit.sh <dir>}"   # require an argument

if [ ! -d "$TARGET" ]; then
  echo "error: '$TARGET' is not a directory" >&2
  exit 1
fi

echo "== World-writable files (anyone can modify) =="
# -perm -0002 matches files where the 'other write' bit is set
find "$TARGET" -xdev -type f -perm -0002 2>/dev/null || true

echo
echo "== setuid / setgid programs (run as owner/group) =="
# -4000 = setuid, -2000 = setgid; -o means logical OR
find "$TARGET" -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null || true

echo
echo "== Private keys readable by group or other (should be 600) =="
find "$TARGET" -xdev -type f -name 'id_*' ! -name '*.pub' -perm /0077 2>/dev/null || true

echo
echo "Audit complete. Review each result; nothing was changed."

What it does. Given a directory, it prints three categories that every permission review checks: world-writable files, setuid/setgid binaries, and private-key files that are readable beyond their owner. find ... -perm -0002 matches the "other write" bit; -perm /0077 matches any group/other access bit being set.

Expected output (illustrative, on a directory with one bad file):

== World-writable files (anyone can modify) ==
/home/ana/project/shared.conf

== setuid / setgid programs (run as owner/group) ==

== Private keys readable by group or other (should be 600) ==
/home/ana/project/keys/id_rsa

Audit complete. Review each result; nothing was changed.

Edge cases. 2>/dev/null hides "Permission denied" noise for subtrees you cannot enter (a normal, expected limitation). -xdev keeps the scan on one filesystem so it does not wander into mounted disks or /proc. The script intentionally takes no action; remediation (e.g. chmod 600) is a deliberate, separate step a human decides on.

Line by line

Walking through perm-audit.sh:

Line / piece What happens
set -euo pipefail Makes the script fail fast: -e exits on any command error, -u errors on an undefined variable, pipefail catches failures inside pipelines. This prevents a half-broken audit from looking like a clean one.
TARGET="${1:?usage...}" Reads the first argument. The :? form prints the usage message and exits if no argument was given, so the script never runs against the wrong place.
if [ ! -d "$TARGET" ] Confirms the target is actually a directory before scanning; otherwise it reports an error to stderr and exits non-zero.
find ... -perm -0002 The leading - on -0002 means "all of these bits are set." Bit 2 in the last octal digit is other write, so this matches every world-writable file.
\( -perm -4000 -o -perm -2000 \) Grouped condition: setuid (4000) or setgid (2000). The backslashes protect the parentheses from the shell so find sees them.
-name 'id_*' ! -name '*.pub' Targets private key files (id_rsa, id_ed25519) while excluding their public .pub counterparts, which are safe to share.
-perm /0077 The leading / means "any of these bits set." 0077 covers every group and other permission bit, so this flags a key that is readable or writable by anyone but the owner.
2>/dev/null || true Discards permission-denied warnings and keeps the script from aborting (under set -e) just because find returned non-zero on an unreadable subtree.

Trace example. Suppose keys/id_rsa has mode 640 (owner rw, group r, other none). The bitmask 0077 includes the group-read bit, mode 640 has the group-read bit set, so -perm /0077 matches and the file is printed. If the key were 600, no group/other bit is set, the match fails, and it is correctly not reported.

Common mistakes

Mistake 1 — Reaching for chmod 777.

# WRONG: "the upload kept failing so I opened it up"
chmod -R 777 /var/www/uploads

This makes every file world-writable: any local user (or a compromised web process) can overwrite content or drop a malicious script. The real problem is almost always ownership, not a lack of permission.

# CORRECT: give the right user/group access, keep others out
chown -R www-data:www-data /var/www/uploads
chmod -R 750 /var/www/uploads

Recognize it: any 777 in a deployment script or ls -l output (rwxrwxrwx) deserves scrutiny.

Mistake 2 — Misreading octal digit order. People sometimes think 750 means "7 for everyone" or read the digits right-to-left. It is always owner, group, other, left to right. 750 = owner 7 (rwx), group 5 (r-x), other 0 (none).

Mistake 3 — Forgetting directory execute.

# WRONG: read on the dir but no execute -> still can't open files
chmod 644 /srv/shared        # rw-r--r-- on a DIRECTORY

Without the execute bit you cannot traverse into the directory, so even readable files inside become unreachable.

# CORRECT: directories that should be enterable need execute
chmod 755 /srv/shared

Recognize it: "Permission denied" when opening a file even though the file's own bits look fine — check every parent directory's execute bit.

Mistake 4 — Leaving a private key group/other readable.

# WRONG
chmod 644 ~/.ssh/id_ed25519   # everyone can read your private key
# CORRECT
chmod 600 ~/.ssh/id_ed25519   # owner only; SSH will otherwise refuse to use it

Recognize it: SSH printing "WARNING: UNPROTECTED PRIVATE KEY FILE" is the system enforcing this for you.

Mistake 5 — Adding setuid to a script as a shortcut. Marking your own helper setuid-root to let it touch a system file is a classic way to create a privilege-escalation hole, and most kernels ignore setuid on scripts anyway. Use a narrowly scoped sudo rule or a proper service instead.

Debugging tips

"Permission denied" when reading or running a file.

  1. Run ls -l file and id (to see your user and groups). Decide which triad applies: are you the owner? in the group? otherwise other.
  2. Remember first match wins — owner bits override group/other for the owner.
  3. Check every parent directory has execute (x). A missing x upstream blocks everything below it: ls -ld /a /a/b /a/b/c.

"Permission denied" when deleting a file you can read. Deletion depends on the directory's write+execute bits, not the file's. Check ls -ld on the containing directory; also check for the sticky bit (t) on shared dirs like /tmp, which restricts deletion to owners.

A program won't run. Confirm the execute bit (chmod +x if you own it) and that it is on a filesystem not mounted noexec. For scripts, check the #! shebang line points to an interpreter that exists.

Octal isn't matching what you expect. Re-derive it: r=4, w=2, x=1, summed per triad, in owner/group/other order. Use stat -c '%a' file (Linux) or stat -f '%A' file (macOS) to print the numeric mode and compare.

Questions to ask when it doesn't work:

  • Who owns this file and what group is it in? Who am I (id)?
  • Which triad does the kernel actually use for me?
  • Do all the parent directories let me traverse in?
  • Is a special bit (setuid/setgid/sticky) or a mount option (noexec, read-only) involved?

Memory safety

This is concept-track material with no C memory model, so the focus is robustness and validation when you write code or scripts that touch permissions:

  • Validate input paths. A script that takes a path argument should confirm it exists and is the expected type before acting, exactly as the example checks -d "$TARGET". This avoids acting on the wrong target.
  • Default to least privilege. Create files with the tightest mode that still works (600 for secrets, 750 for group programs) rather than opening them up and "locking down later." Later rarely comes.
  • Never make secrets world-readable. Private keys, tokens, and password files should be 600 and owned by the right account. World-readable secrets are equivalent to no secret at all on a multi-user box.
  • Be conservative with privilege. Avoid setuid where a scoped sudo rule or a dedicated service account will do. If a service must start as root to bind a low port, have it drop privileges immediately afterward.
  • Audit, don't auto-fix. Read-only review (like the example) is safe to run broadly; bulk chmod -R/chown -R can lock you out or expose data if a wildcard is wrong. Make changes deliberately, on reviewed results.
  • Ethics / authorization. Only inspect and modify permissions on systems and files you own or are explicitly authorized to administer. Permission auditing on machines that are not yours, even when read-only, can be unauthorized access.

Real-world uses

Where this shows up in real systems:

  • Web servers. Document roots are typically 755/644 owned by the web user; secrets and TLS private keys are 600. A world-writable web directory is a classic foothold for attackers.
  • SSH. OpenSSH refuses to use a private key or even your ~/.ssh directory if the permissions are too open — the tool actively enforces 600/700.
  • Shared team directories. A setgid directory with the sticky bit lets a team collaborate while keeping new files in the shared group and stopping members from deleting each other's work.
  • System binaries. /usr/bin/passwd, /usr/bin/sudo, and ping (historically) are setuid root so unprivileged users can perform tightly scoped privileged actions.
  • Security reviews & CI hardening. Permission audits — world-writable files, stray setuid binaries, over-readable secrets — are standard items in CIS benchmarks and automated compliance scans.

Professional best-practice habits

Beginner rules:

  • Use the least permissive mode that works; reach for chmod 777 essentially never.
  • Keep secrets at 600, programs/directories at 755 (or 750 when access should be limited).
  • Always check parent-directory execute bits when access fails.
  • Read modes carefully: type char, then owner/group/other, left to right.

Advanced habits:

  • Set ownership and group deliberately; rely on group membership rather than loosening "other."
  • Treat every setuid/setgid binary as security-sensitive; minimize them and review their code.
  • Have privileged services drop to an unprivileged account after startup.
  • Make permission auditing repeatable and read-only; separate detection from remediation so changes are intentional and reviewed.

Practice tasks

Beginner 1 — Translate modes. Convert each symbolic permission to octal and vice versa: (a) -rwxr-xr--, (b) -rw-r-----, (c) 640, (d) 700. Requirements: show the per-triad arithmetic (r=4, w=2, x=1). Hint: work one triad at a time, owner then group then other. Concepts: rwx bits, octal modes.

Beginner 2 — Predict access. A file report.txt is owned by ana, group staff, mode 640. For each user decide read/write/neither and say which triad applies: (a) ana; (b) bob, who is in staff; (c) carol, in no relevant group. Requirements: name the triad used for each and remember first-match-wins. Concepts: owner/group/other, triad selection.

Intermediate 1 — Directory traversal puzzle. You have /srv/data (mode 711, owned by root) containing notes.txt (mode 644, owned by root). As an ordinary user: can you ls /srv/data? Can you cat /srv/data/notes.txt? Explain each answer. Hint: distinguish read vs execute on the directory. Concepts: directory rwx semantics.

Intermediate 2 — Lock down a secrets folder. Write the chmod/chown commands to make ~/app/secrets a directory only its owner can enter (no group or other access), containing files only the owner can read or write. State the final octal modes for the directory and the files, and explain why each digit is what it is. Constraints: do not use 777; do not give other any bits. Concepts: least privilege, file vs directory permissions.

Challenge — Build a read-only permission auditor. Extend the lesson's perm-audit.sh idea: write a script that takes a directory and reports, in clearly labeled sections, (1) world-writable files, (2) setuid and setgid programs (labeled separately), and (3) files where other has any access but the owner's home would normally keep private. Requirements: validate the argument is a directory; make no changes; exit non-zero on bad input; suppress permission-denied noise; stay on one filesystem (-xdev). Provide an example invocation and sample output. Constraints: read-only, authorized targets only. Hint: find -perm -MODE means "all bits set", -perm /MODE means "any bit set". Concepts: special bits, world-writable detection, defensive scripting. (Do not just copy the lesson script — add the separate setuid/setgid labeling and your own private-file check.)

Summary

  • On Unix, everything lives in one tree rooted at /, addressed by absolute or relative paths.
  • Each file has an owner, a group, and an other category; the kernel applies exactly one triad, first match wins (owner, then group, then other).
  • Permissions are three rwx triads. They mean different things on files (read contents / change contents / run) versus directories (list names / modify entries / enter and traverse).
  • Modes are written in octal: r=4, w=2, x=1, summed per triad — so -rwxr-x--- is 750. Watch for 777 (world-writable) as a red flag and keep secrets at 600.
  • The special bits — setuid (run as owner), setgid (run as group / inherit group on dirs), and sticky (deletion-restricted shared dirs) — appear as a leading octal digit and as s/t in the listing. setuid binaries are powerful and security-sensitive.
  • root (UID 0) bypasses all checks, which is why services should drop privileges and why mis-set permissions and stray setuid binaries are recurring audit findings.
  • Common mistakes: chmod 777, misreading digit order, forgetting directory execute, leaving secrets world-readable, and abusing setuid. When access fails, check ownership, the right triad, and every parent directory's execute bit.

Practice with these exercises