Linux Permissions and Users

Linux inherits the classic Unix permission model: users, groups, and read / write / execute bits on three classes (user, group, other). It’s simple enough to memorise in five minutes, powerful enough for most cases, and extended by ACLs, capabilities, and MAC systems (SELinux / AppArmor) when you need more.

Users and groups

Everything runs as a user (UID), belongs to a primary group (GID), and optionally joins many supplementary groups.

The source of truth

Plain text files in /etc:

FileHolds
/etc/passwdUser records (UID, GID, home, shell) — world-readable
/etc/shadowPassword hashes + aging — root-only
/etc/groupGroups + membership
/etc/gshadowGroup passwords (rarely used)
/etc/sudoers + /etc/sudoers.d/*Who can run what as whom

Example /etc/passwd line:

alice:x:1000:1000:Alice Example:/home/alice:/bin/bash
│     │ │    │    │             │           │
user  │ UID  GID  GECOS (desc)  home        login shell
      password placeholder (x = in shadow)

The x in the second field means “see /etc/shadow” for the real hash.

Special UIDs

UIDWho
0root — bypasses all permission checks
1-99 (Debian) / 1-999 (RHEL)System users for daemons (no login shell, no home)
1000+Human users
65534nobody — the “no privileges” fallback

The “service account” pattern

Daemons should not run as root. Standard pattern:

useradd --system --no-create-home --shell /usr/sbin/nologin myapp
chown -R myapp:myapp /var/lib/myapp
# run the service as User=myapp in the systemd unit

This is what distro packages do for nginx, postgres, etc. Replicate it for anything custom.

The permission bits

ls -l file
-rwxr-xr--  1  alice  devs  4096  Apr 24 12:00  file
│└─┬─┘└─┬─┘└┬┘
│  u    g   o
└── file type (- file, d dir, l symlink, c char, b block, s socket, p pipe)

Three classes × three bits:

ClassWho
u serThe owner
g roupMembers of the file’s group
o therEveryone else

Three bits per class:

BitOn a fileOn a directory
r eadRead contentsList entries (ls)
w riteModify contentsCreate / delete / rename entries
e xecuteRun as programTraverse into it (enter with cd)

The directory-execute bit is the trap: without it, you can’t even cd into the dir; with it but no read bit, you can open files inside if you know their names but can’t list them. A web-server user often gets --x on a path: walk through, but no listing.

Numeric (octal) shorthand

Each class is a digit 0–7:

r = 4    w = 2    x = 1

Sum the bits:

rwxOctal
---0
r--4
rw-6
r-x5
rwx7

Common modes:

UseModeLiteral
Private file600rw-------
Private key (SSH)600rw-------
Script755rwxr-xr-x
World-readable config644rw-r--r--
Private dir700rwx------
Shared dir755rwxr-xr-x
/tmp-style dir1777rwxrwxrwt (sticky bit)

Changing ownership and mode

chmod 755 file                  # set absolute mode (numeric)
chmod u+x,g-w file              # modify specific bits (symbolic)
chmod -R 750 /srv/app           # recursive
 
chown alice file                # change owner
chown alice:devs file           # owner and group
chown -R myapp:myapp /var/lib/myapp
chgrp devs file                 # change only group

Symbolic vs numeric

Symbolic (u+x, g-w, o=r) is diff-friendly — it changes only specified bits. Numeric (755) is absolute — it overwrites. Prefer symbolic when you’re tweaking; numeric when you want a known end-state.

The three “special” bits

Beyond rwx, there are three magic bits that sometimes matter:

setuid (s) — octal 4000

On an executable: when run, the process takes on the owner’s UID, not the caller’s. passwd is the classic example: regular users can change their own password, which requires writing /etc/shadow, which requires root — setuid root on /usr/bin/passwd makes it work.

ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208  /usr/bin/passwd
   ^ s instead of x

Security concern: setuid root on a program with a bug is a privilege-escalation vector. find / -perm -4000 to audit.

setgid (s) — octal 2000

On an executable: run as the file’s group. On a directory: new files inherit the directory’s group (useful for shared folders).

chmod g+s /srv/shared
# now every file created in /srv/shared is group=devs

sticky bit (t) — octal 1000

On a directory: only the file’s owner (or root) can delete the file, even if others have write on the directory. That’s how /tmp works — everyone can write there, but you can only delete your own stuff.

ls -ld /tmp
drwxrwxrwt 18 root root 4096 /tmp
         ^ t

umask — default permission mask

When you create a file, the mode is 0666 & ~umask; for a dir it’s 0777 & ~umask. Default umask is typically 022:

  0666
& ~022   (= 0644)
-------
  0644    → rw-r--r--

Check / set:

umask           # show current (e.g. 0022)
umask 077       # tighter: rwx for owner only, nothing else

Systemd services: UMask= in the [Service] section.

sudo — the real privilege escalation

sudo lets a user run commands as another user (default: root), subject to policy in /etc/sudoers and files in /etc/sudoers.d/.

Editing sudoers

Always use visudo (or visudo -f /etc/sudoers.d/myfile). It parses the file before saving — a syntax error in sudoers can lock you out of root access.

Common policies

# Everyone in wheel gets full sudo
%wheel  ALL=(ALL:ALL) ALL
 
# No password for a specific user (dangerous — use sparingly)
alice   ALL=(ALL) NOPASSWD: ALL
 
# Let the deploy user restart exactly one service
deploy  ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service
 
# Aliases for readable policy
Cmnd_Alias SAFE = /usr/bin/tail -f *, /usr/bin/journalctl -u *
readonly ALL=(root) NOPASSWD: SAFE

Useful habits:

  • Use sudo -i for an interactive root shell (loads root’s env).
  • Use sudo !! to re-run the last command with sudo.
  • Check what you can do: sudo -l.
  • Never sudo chmod -R 777 anything you don’t understand.

Groups in practice

Add a user to a group:

usermod -aG docker alice         # -a = append (vital — without -a you REPLACE group list)

After this the user needs to log out / log back in (or newgrp docker) for the change to apply.

Common “power” groups:

GroupTypical rights
wheel / sudoUse sudo (distro-dependent)
admRead /var/log/*
dockerTalk to the Docker daemon (effectively root — be careful who you add)
dialoutSerial ports
diskRaw block devices (≈ root)
plugdev, video, audioHardware access for desktop users

ACLs — when three classes aren’t enough

Sometimes “user / group / other” is too coarse. Solution: POSIX ACLs give per-user and per-group grants on top of the classic bits.

# Grant bob read+write to a file owned by alice:devs
setfacl -m u:bob:rw file
getfacl file
 
# Default ACL on a directory — applies to new files inside
setfacl -m d:u:bob:rw /srv/shared

Enable on ext4 if needed: mount with acl option (most distros do by default).

When ls -l shows a + at the end of the mode, an ACL is in effect:

-rw-rw-r--+ 1 alice devs 4096 … file
         ^ACL present

Capabilities — finer than setuid

Modern Linux splits root’s power into ~40 capabilities (e.g. CAP_NET_BIND_SERVICE to bind ports <1024, CAP_NET_RAW for raw sockets, CAP_SYS_ADMIN the omnibus one). You can grant a single capability to a binary instead of full setuid root:

setcap cap_net_bind_service=+ep /usr/local/bin/myapp
# now myapp can bind :80 as an unprivileged user
getcap /usr/local/bin/myapp

Or at the systemd level:

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

This is how modern services bind low ports without ever being root.

SELinux / AppArmor — the layer above DAC

The classic bits are DAC (Discretionary Access Control) — the owner decides. MAC (Mandatory Access Control) sits above: even root can be blocked by policy.

  • SELinux — default on RHEL-family. Label-based. getenforce, setenforce 0|1, sestatus, audit2allow.
  • AppArmor — default on Debian/Ubuntu/SUSE. Path-based. aa-status, profiles in /etc/apparmor.d/.

When something works as root but not for a user, and permissions look fine, SELinux/AppArmor is the usual culprit. Check the audit log:

# SELinux
ausearch -m avc -ts recent
# AppArmor
journalctl -k | grep apparmor

SSH key auth — where permissions actually bite you

SSH is strict about file modes. These trip people up constantly:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
chmod 600 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/id_rsa.pub
chmod 644 ~/.ssh/known_hosts

If ~/.ssh is group-writable, sshd silently refuses to honour authorized_keys. /var/log/auth.log (or journalctl -u sshd) will show the reason.

Quick checklist — “why can’t X read this file?”

  1. ls -la the file and every parent directory. X needs x on each parent.
  2. id X — confirm their UID and group membership.
  3. Getfacl the file if + shown in mode.
  4. Check the filesystem mount — is it read-only (mount | grep)? Mounted with nosuid / noexec?
  5. Owner might be a numeric UID (no matching /etc/passwd entry) — ls -ln.
  6. SELinux/AppArmor denial? Check audit logs.
  7. On NFS: UIDs must match (or use ID mapping / Kerberos).

See also