Skip to content

The Filesystem & Permissions

A process needs to read configuration, write logs, and be denied access to things that aren’t its business. All three of those happen through the filesystem and its permission model. If the process model is “what is running,” this page is “what it’s allowed to touch.” That second question is where most security incidents — and a surprising number of outages — are won or lost.

This is the deepest idea in Unix, and it’s not a slogan. On Linux, almost everything is exposed through the filesystem as if it were a file:

  • A regular file? A file.
  • A directory? A special file listing other files.
  • A hard disk, a terminal, a keyboard? Files under /dev.
  • A running process’s details? Files under /proc/<pid>.
  • A network socket, a pipe? File-like objects with file descriptors.

Why does this matter for DevOps? Because it means one set of tools and one permission model governs everything. The same cat, grep, and chmod that work on a config file work on a device or a kernel setting. You don’t learn a new API for each kind of resource; you learn the filesystem once. That uniformity is what makes the shell so powerful as automation glue.

The Filesystem Hierarchy Standard (a quick tour)

Section titled “The Filesystem Hierarchy Standard (a quick tour)”

Linux doesn’t scatter files randomly; it follows the FHS, a convention for what goes where. Knowing the map means you can walk onto any Linux server and know where to look.

/ the root of everything (not the root user's home!)
├── bin, /usr/bin executable programs (commands)
├── etc system-wide CONFIGURATION (text files — read these first)
├── var
│ ├── log LOGS live here → /var/log
│ └── lib application state/data
├── home
│ └── alice a normal user's home directory (~)
├── root the root user's home directory
├── tmp temporary files (often wiped on reboot)
├── opt optional / third-party software
├── proc the kernel's live view of processes (virtual)
└── dev devices (disks, terminals) as files

Three to burn into memory: /etc (configuration — where you change behavior), /var/log (where you go when something breaks, see Logs & Troubleshooting), and /proc (a live, virtual window into the kernel and every running process).

Every process runs as a user, and every user belongs to one or more groups. Permissions are decided by asking: is the requester the file’s owner, a member of the file’s group, or everyone else? Three identities, checked in that order.

Terminal window
id # who am I? shows my user (uid) and groups (gids)
whoami # just the username
groups alice # which groups alice belongs to

Users and groups are how you give a service exactly the access it needs and nothing more. Your database runs as the postgres user; your web app as www-data. If the web app is compromised, the attacker has the web app’s access — not the database’s, not root’s. That blast-radius limiting is the entire point.

Each file carries permissions for three classes (owner / group / other), each with three possible rights:

BitOn a fileOn a directory
r (read, 4)view contentslist what’s inside
w (write, 2)modify contentscreate/delete entries inside
x (execute, 1)run it as a programenter (cd into) it

ls -l shows them as a 10-character string:

-rwxr-x--- 1 alice devs 4096 Jun 22 10:00 deploy.sh
│└┬┘└┬┘└┬┘ └─┬─┘ └─┬┘
│ │ │ │ │ └ group: devs
│ │ │ │ └────── owner: alice
│ │ │ └ other: --- (no access at all)
│ │ └─── group: r-x (read + execute)
│ └────── owner: rwx (read + write + execute)
└──────── type: - = file, d = directory, l = symlink

So alice can edit and run deploy.sh, anyone in devs can read and run it, and everyone else is locked out completely.

Add the values per class — r=4, w=2, x=1 — and you get the octal shorthand you’ll see everywhere:

rwx = 7 rw- = 6 r-x = 5 r-- = 4 --- = 0

So rwxr-x--- is 750. Common ones worth memorizing: 644 (rw-r--r--, a normal file: owner edits, others read), 600 (rw-------, private — use this for secrets and SSH keys), and 755 (rwxr-xr-x, a program or directory everyone may use but only the owner may change).

Changing ownership and permissions: chown and chmod

Section titled “Changing ownership and permissions: chown and chmod”
Terminal window
chmod 600 ~/.ssh/id_ed25519 # lock a private key to owner-only (ssh refuses looser perms)
chmod +x deploy.sh # add execute so you can run ./deploy.sh
chmod -R g+w /var/www # recursively give the group write access
chown www-data:www-data app.log # set owner AND group to www-data
chown -R alice /home/alice/site # recursively change owner

The root user, and why you mostly avoid it

Section titled “The root user, and why you mostly avoid it”

root (uid 0) is the superuser: it bypasses every permission check. root can read any file, kill any process, and overwrite the kernel’s settings. That power is also the danger — a typo as root (rm -rf / with a stray space) can erase the machine, and a process compromised while running as root hands the attacker the whole box.

So the discipline is: don’t log in as root, and don’t run services as root. Instead, work as a normal user and elevate for the one command that needs it with sudo:

Terminal window
apt update # fails: needs privilege to write system files
sudo apt update # runs that single command as root, then drops back
sudo -u postgres psql # run a command as a specific other user

sudo is better than a root login for three reasons that are pure DevOps: it’s auditable (every sudo is logged to /var/log/auth.log, or /var/log/secure on RHEL — you know who did what), it’s scoped (one command, not a whole session), and it’s grantable (you can let a teammate restart one service without handing over the keys to the kingdom).

Least privilege: the principle underneath all of it

Section titled “Least privilege: the principle underneath all of it”

Every choice above — separate users per service, narrow rwx bits, sudo instead of root — is one principle wearing different clothes: give each user and process the minimum access it needs to do its job, and no more.

This is the manual-step thread of this page. The error-prone habit is granting broad access “to make the error go away” — running everything as root, chmod 777 on the data directory, one shared admin login. It works right up until something is compromised or someone fat-fingers a command, and then the blast radius is everything. Least privilege is how you make production safer by default: when the worst happens, it stays small. The same principle scales up later into RBAC and network policy and the zero-trust stance — but it starts right here, with file bits and sudo.

Under the hood — the fourth bit: setuid, setgid, and the sticky bit

Section titled “Under the hood — the fourth bit: setuid, setgid, and the sticky bit”

The rwx triplets are only three of the four octal digits. There’s a leading fourth — setuid (4), setgid (2), and the sticky bit (1) — and it answers a puzzle: how can an ordinary user change their own password, when that means writing to root-owned /etc/shadow?

The answer is setuid. A program with the setuid bit runs with the privileges of the file’s owner, not the user who launched it. /usr/bin/passwd is owned by root and is setuid, so running it briefly grants root — just long enough to update one file. You’ll spot it in ls -l as an s where the owner’s x would be:

-rwsr-xr-x 1 root root /usr/bin/passwd ← the 's' = setuid root
drwxrwxrwt 1 root root /tmp ← the 't' = sticky bit

The sticky bit on a world-writable directory like /tmp fixes a different problem: everyone may create files, but only a file’s owner (or root) may delete it — so users can’t delete each other’s temp files. And setuid is double-edged: a bug in any setuid-root program is an instant road to root, which is why such programs are a favourite target (recall Dirty COW) and why you keep them to a minimum — audit with find / -perm -4000. setuid is least privilege done with a scalpel: grant the exact extra power for the exact operation, instead of handing the user root.

The permission model is something you configure deliberately on every machine, so it earns the five questions:

  • Why does it exist? Because every process must read its config, write its logs, and be denied what isn’t its business — and “everything is a file” lets one permission model (owner / group / other × rwx) govern regular files, devices under /dev, processes under /proc, and sockets alike.
  • What problem does it solve? It limits blast radius: running each service as its own user (postgres, www-data) means a compromise gets only that service’s access — not the database’s, not root’s.
  • What are the trade-offs? Fine-grained control demands discipline — the lazy “fix” (chmod 777, run-everything-as-root, one shared admin login) makes the error vanish but strips away the very protection that contains a breach.
  • When should I loosen it? Almost never broadly; grant the specific user or group the specific right, reach for a scalpel like setuid (audited with find / -perm -4000) only when truly needed, and elevate per-command with sudo rather than logging in as root.
  • What breaks if I ignore least privilege? A single compromised process — or a fat-fingered rm -rf / as root — takes the whole box; the same principle scales up into RBAC and network policy and the zero-trust stance.
  1. “Everything is a file.” Give two non-file things Linux exposes as files, and explain why that uniformity helps automation.
  2. You see -rw-r-----. Who can read this file, who can write it, and what is the octal form?
  3. A service throws a permission error reading /etc/app/config.yml. Why is chmod 777 the wrong fix, and what would you do instead?
  4. Give three concrete reasons sudo apt update is preferable to logging in as root and running apt update.
  5. State the principle of least privilege in one sentence, then connect it to blast radius: how does running each service as its own user limit the damage from a single compromise?
Show answers
  1. Open-ended — e.g. a running process is exposed under /proc/<pid>, and a device (disk, terminal) under /dev; sockets and pipes are file-like with file descriptors. The uniformity helps automation because one set of tools and one permission model (cat, grep, chmod) governs everything — you learn the filesystem once instead of a new API per resource.
  2. -rw-r----- means the owner can read and write, the group can read only, and everyone else has no access. Owner rw- = 6, group r-- = 4, other --- = 0, so the octal form is 640.
  3. chmod 777 lets anyone read, write, and execute the file — it removes the very protection that contains a breach, so it’s almost always wrong. The right fix is to grant the specific user or group (e.g. the service’s user) the specific right it needs — here, ensure the service’s user can read /etc/app/config.yml, e.g. via ownership or a 640 file owned by that user/group.
  4. (1) Auditable — every sudo is logged to /var/log/auth.log, so you know who did what. (2) Scoped — it elevates for one command, then drops back, rather than a whole root session. (3) Grantable — you can let a teammate run one privileged command without handing over full root.
  5. Give each user and process the minimum access it needs and no more. Running each service as its own user limits blast radius: if the web app (running as www-data) is compromised, the attacker has only the web app’s access — not the database’s, not root’s — so a single compromise stays contained instead of taking the whole box. It scales up later into RBAC and network policy.