cheat sheet

permissions

POSIX file permissions on Linux. Covers symbolic and octal chmod, ownership with chown, the meaning of r/w/x for files vs. directories, setuid/setgid/sticky bits, umask, ACLs, capabilities, and immutable attributes.

permissions — chmod, chown, umask, ACLs

What it is

Linux file permissions are a POSIX-standard access-control model that assigns read, write, and execute bits separately for the file's owner, its group, and others. Every file and directory carries an owner UID, a group GID, and a 12-bit mode (9 permission bits plus 3 special bits: setuid, setgid, sticky). The toolchain is small but the semantics are subtle — directory bits don't mean what file bits mean, the umask affects every new file, and ACLs add a parallel system on top. Reach for chmod / chown / umask for everyday work; layer in setfacl when POSIX owner/group/other isn't expressive enough, and chattr for kernel-level immutability.

Reading permissions with ls -l

ls -l shows mode and ownership as the first two columns. The mode is a 10-character string: one type indicator followed by three triplets of rwx for owner, group, and other. A dash means "bit unset".

bash
ls -l /home/alice

Output:

text
-rw-r--r-- 1 alice  staff   2048 May 24 10:00 notes.txt
drwxr-xr-x 5 alice  staff    160 May 24 10:00 projects
-rwxr-x--- 1 alice  devs    8192 May 24 10:00 deploy.sh
lrwxrwxrwx 1 alice  staff      9 May 24 10:00 latest -> notes.txt
CharacterMeaning
- (position 1)regular file
ddirectory
lsymbolic link
c / bcharacter / block device
s / psocket / named pipe
r w x (positions 2–4)owner permissions
r w x (positions 5–7)group permissions
r w x (positions 8–10)other (everyone else) permissions

The 1 after the mode is the link count, then owner, group, size in bytes, mtime, and name.

r/w/x for files vs. directories

The same three letters mean different things on files and directories. This is the single most common source of permission bugs.

BitOn a fileOn a directory
rRead contentsList entries (i.e. ls)
wModify contentsCreate, rename, delete entries
xExecute as programTraverse (i.e. cd, look up entries by name)

A directory with r but no x lets you see that files exist but not access their metadata; with x but no r you can cat /dir/file if you already know the name, but ls /dir fails. Almost every directory you create should have x for whomever needs to enter it.

bash
# Useful sanity check: this lets group members enter and read,
# but only the owner can modify.
chmod 750 /home/alice/secret-notes

# In contrast: this is what a public assets dir wants.
chmod 755 /var/www/static

Output: (none — exits 0 on success)

Octal notation

Each permission triplet maps to one octal digit: r=4, w=2, x=1. Add them to set the bits you want. A full mode is three (sometimes four) octal digits: optional special bits, then owner/group/other.

OctalPermissionsCommon use
0---no access
1--xexecute / traverse only
4r--read only
5r-xread + execute (executables, public dirs)
6rw-read + write (data files)
7rwxfull access

Common combinations:

ModeMeans
644owner rw, others r — text files
600owner rw only — secrets, SSH keys
755owner rwx, others rx — scripts, public dirs
700owner rwx only — private dirs
750owner full, group rx, others none — group-shared scripts
775owner+group full, others rx — group-writable web roots
2775as above plus setgid — group inheritance (see below)
1777rwx for all, sticky bit — /tmp
bash
chmod 644 notes.txt           # rw-r--r--
chmod 755 deploy.sh           # rwxr-xr-x
chmod 600 ~/.ssh/id_ed25519   # rw-------  (sshd requires this)
chmod 700 ~/.ssh              # rwx------

Output: (none — exits 0 on success)

Symbolic notation

Symbolic mode is additive and relative. The syntax is WHO OPERATOR PERMS, where:

  • WHO is u (user/owner), g (group), o (other), or a (all = ugo).
  • OPERATOR is + (add), - (remove), or = (set exactly).
  • PERMS is any combination of r, w, x (and the special bits s, t, X).

Symbolic mode is preferred when you want to change a single bit without thinking about the others — for example, "make this script executable" without disturbing read permissions.

bash
chmod +x deploy.sh            # add execute for everyone (subject to umask)
chmod u+x,go-w deploy.sh       # exec for owner; remove write from group/other
chmod a-w shared.conf          # nobody can write
chmod go= secret.key           # group and other get nothing
chmod u=rw,g=r,o= notes.txt    # set exact bits (equivalent to 640)

# X (capital) = add x only where it's already set or on directories
chmod -R u+rwX,go+rX project/  # safe recursive: don't make data files executable

Output: (none — exits 0 on success)

Use capital X (not lowercase x) when recursing with chmod -R. Lowercase x makes every file executable, including text and images. Capital X only adds the execute bit where one of the triplets already has it or where the target is a directory.

Setuid, setgid, and sticky

These three "special" bits live above the standard nine and change how an executable runs or how a directory creates new entries.

BitSymbolOctalEffect
setuids in user-x4000On executable: process runs as the file's owner (not the invoker).
setgids in group-x2000On executable: process runs as the file's group. On directory: new entries inherit the directory's group.
stickyt in other-x1000On directory: only the file's owner (or root) can rename / delete entries.
bash
ls -l /usr/bin/passwd /tmp

Output:

text
-rwsr-xr-x 1 root  root  68208 Apr 14 12:00 /usr/bin/passwd
drwxrwxrwt 5 root  root    160 May 24 09:30 /tmp

The s in rws... marks passwd as setuid: when alice runs it, the process effectively runs as root (so it can update /etc/shadow). The t in rwxrwxrwt is the sticky bit on /tmp: everyone can write there, but you can only delete files you own.

bash
# Setuid binary (rare in your own code; usually root-installed)
chmod u+s /usr/local/bin/myhelper
chmod 4755 /usr/local/bin/myhelper      # equivalent: 4 prefix = setuid

# Setgid directory — new files inherit the dir's group, useful for shared dirs
chgrp devs /srv/shared
chmod g+s /srv/shared
chmod 2775 /srv/shared                   # equivalent: 2 prefix = setgid

# Sticky bit on a shared writable directory
chmod +t /srv/public-uploads
chmod 1777 /srv/public-uploads           # equivalent: 1 prefix = sticky

Output: (none — exits 0 on success)

[!WARN] Setuid scripts (shebang-launched shell or Python files) are ignored by the Linux kernel for security reasons. Setuid only takes effect on compiled binaries. To run a script with elevated privileges, write a small C wrapper or use sudo with a specific rule in /etc/sudoers.d/.

chown — change owner and group

chown USER:GROUP path sets both at once; omitting either part changes only the half you specify. Only root can change a file's owner; a regular user can change the group to one they belong to. Use -R to recurse and -h to act on a symlink itself rather than its target.

bash
sudo chown alice notes.txt              # change owner only
sudo chown alice:devs deploy.sh         # owner + group
sudo chown :devs shared.log             # group only (note leading colon)
sudo chown -R alice:devs /srv/project   # recursive

# Act on the symlink, not its target
sudo chown -h alice /var/run/myapp.sock

# Copy ownership from another file
sudo chown --reference=template.conf newfile.conf

Output: (none — exits 0 on success)

chgrp — change group only

chgrp is a convenience for the group-only case. The user must belong to the destination group (or be root).

bash
chgrp devs deploy.sh
chgrp -R devs /srv/project

Output: (none — exits 0 on success)

umask — default permissions for new files

umask is a per-shell mask of bits that are removed from the mode given to open(2) and mkdir(2) when a new file or directory is created. The default file mode requested by most programs is 666 (rw for everyone); the default directory mode is 777. The umask is subtracted from those.

umaskNew file modeNew dir modeTypical for
022644755most distros' default
002664775shared group workflow
077600700single-user, strict
027640750server processes
bash
umask                  # show current value
umask 077              # strict — only owner can read new files
touch private.txt
ls -l private.txt

Output:

text
0022
-rw------- 1 alice  staff 0 May 24 10:00 private.txt

Set the umask in your shell init file (~/.bashrc, ~/.zshrc) so it's persistent across sessions. For systemd-managed services, set UMask= in the unit's [Service] section.

bash
# ~/.bashrc
umask 027

Output: (none — exits 0 on success)

ACLs — fine-grained permissions

POSIX Access Control Lists add per-user and per-group permissions on top of the three-class owner/group/other model. Use ACLs when you need "alice and bob can write, devs can read, everyone else nothing" — something the three-class model can't express.

bash
# Install (most distros ship userspace tools already)
sudo apt install acl              # Debian/Ubuntu
sudo dnf install acl              # Fedora

# View the ACL on a file (the `+` after `ls -l` mode column means an ACL exists)
getfacl shared.log

Output:

text
# file: shared.log
# owner: alice
# group: devs
user::rw-
user:bob:rw-
group::r--
mask::rw-
other::---
bash
# Grant bob write access
setfacl -m u:bob:rw shared.log

# Grant the devs group read access
setfacl -m g:devs:r shared.log

# Remove an entry
setfacl -x u:bob shared.log

# Default ACL on a directory — applied to all new files inside it
setfacl -d -m g:devs:rwx /srv/team-share
setfacl -d -m u:alice:rwx /srv/team-share

# Recursive
setfacl -R -m g:devs:rX /srv/team-share

# Wipe all extended ACL entries (return to plain POSIX)
setfacl -b shared.log

Output: (none — exits 0 on success)

ACL mask is an upper bound on what any named user or group can be granted. If you set u:bob:rwx but the mask is rw, bob effectively has rw. setfacl -m m::rwx file raises the mask.

[!WARN] Mask auto-recalculation surprises. Every setfacl -m recomputes the mask as the union of named-user / named-group / owning-group permissions — so a mask you explicitly tightened with m::r will silently widen back to rwx the next time you grant u:alice:rwx. To keep a restrictive mask, pass -n (--no-mask) on subsequent edits, or always re-set the mask last (setfacl -m u:alice:rwx,m::r file).

POSIX ACLs vs. NFSv4 ACLs

setfacl / getfacl operate on POSIX ACLs and do not work on NFSv4 mounts. NFSv4 uses a richer Windows-style ACL model (per-ACE allow/deny, fine-grained rntcyD permissions, inheritance flags) that needs a separate userland — install nfs4-acl-tools and use nfs4_getfacl / nfs4_setfacl on those mounts. The two ACL systems share no syntax; if getfacl /mnt/nfs/file shows only the base permissions with no extended entries even though the server has them, you are almost certainly looking at an NFSv4 share with the wrong tool.

bash
sudo apt install nfs4-acl-tools

# View the NFSv4 ACL
nfs4_getfacl /mnt/nfs/shared/report.txt

# Grant alice generic read (R = rntcy) on an NFSv4 file
nfs4_setfacl -a A::alice@example.com:R /mnt/nfs/shared/report.txt

# Preview an edit without applying it
nfs4_setfacl --test -a A::devs@example.com:RW /mnt/nfs/shared/report.txt

Output: (none — exits 0 on success)

chattr — kernel-level attributes

chattr sets ext2/3/4 filesystem attributes that the kernel enforces below the permission layer — they apply even to root. The most useful are +i (immutable: no rename, delete, write, or link) and +a (append-only: append-only writes are permitted).

bash
sudo chattr +i /etc/resolv.conf       # make it immutable
lsattr /etc/resolv.conf

Output:

text
----i--------e----- /etc/resolv.conf
bash
sudo chattr -i /etc/resolv.conf       # remove immutable
sudo chattr +a /var/log/audit.log     # append-only — protects audit logs

Output: (none — exits 0 on success)

[!WARN] If you can't delete a file even as root, run lsattr first. An i flag from a previous chattr +i is the usual culprit; remove it with chattr -i.

Capabilities

Linux capabilities split root's powers into ~40 discrete units (CAP_NET_BIND_SERVICE, CAP_CHOWN, CAP_SYS_ADMIN, etc.) and can be attached to a binary — or granted to a service via systemd — so it gains a single privilege without ever running as setuid-root. Reach for capabilities when you'd otherwise use setuid-root but only need a narrow ability; this is the modern least-privilege replacement for chmod u+s.

bash
# Allow a non-root process to bind to ports < 1024
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myserver

# View a binary's capabilities
getcap /usr/local/bin/myserver

Output:

text
/usr/local/bin/myserver cap_net_bind_service=ep
bash
# Strip capabilities
sudo setcap -r /usr/local/bin/myserver

# Audit every file capability across the system
sudo getcap -r / 2>/dev/null

Output: (none — exits 0 on success)

The five capability sets

Every process carries five capability sets, and a file can advertise three of them (p, i, e) via setcap. Knowing which letter does what is the difference between a working capability and a baffling EPERM.

SetLetterWhere it livesPurpose
Permittedpprocess + fileThe capabilities a process may exercise — the upper bound.
Effectiveeprocess + file (single bit)The subset currently in force; checked on every syscall. On a file, e is a single flag that says "promote the permitted set to effective at execve".
Inheritableiprocess + fileCapabilities that pass to a child only if the child's file capabilities request them.
BoundingprocessA per-process ceiling — capabilities outside it can never be re-acquired, even via setuid-root.
Ambientprocess (kernel ≥ 4.3)Capabilities preserved across execve of unprivileged binaries. Lets a non-root parent hand a capability to a non-setcap child.

The shorthand cap_net_bind_service=+ep adds CAP_NET_BIND_SERVICE to both permitted and effective — which is what almost every "let this binary bind to port 80" recipe wants. Use =eip if a child process (different binary, no file caps) also needs the privilege via the inheritable path.

systemd: capabilities without touching the binary

For long-running services, prefer systemd's AmbientCapabilities= and CapabilityBoundingSet= over setcap on the executable. The unit grants the capability at process start, so the binary on disk stays plain — no getcap audit surprise, no breakage when a package upgrade replaces the file.

ini
[Service]
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myserver
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

AmbientCapabilities= gives the service the capability even though it runs as myapp (not root); CapabilityBoundingSet= clamps the ceiling so a compromise can't escalate to any other capability; NoNewPrivileges=true blocks setuid binaries from re-elevating. The keep-caps securebit is set automatically when ambient caps are requested, so the capability survives the drop from root to the myapp user at startup.

Ambient ⊆ inheritable ⊆ permitted is enforced by the kernel: you cannot add a capability to the ambient set unless it is already both permitted and inheritable. systemd handles this for you; if you're scripting capsh --caps=… by hand, set permitted and inheritable first, then ambient.

[!WARN] File capabilities live in extended attributes (security.capability). They survive cp -a and rsync -X, but a plain cp or moving across a filesystem that doesn't support xattrs silently strips them — your binary suddenly stops binding to port 80 and there's no error to grep for. Re-run setcap after deploys, or verify with getcap in CI.

Common pitfalls

  1. Forgetting x on directorieschmod 644 -R project/ strips the x from directories and you can no longer cd into them. Use capital X (chmod -R u=rwX,go=rX project/) or apply different modes to files and directories separately.
  2. SSH key with 644sshd refuses to use a private key that is group- or world-readable. chmod 600 ~/.ssh/id_* and chmod 700 ~/.ssh.
  3. Setuid on a script — has no effect; the kernel ignores it for interpreted files. Use sudo or compile a small C wrapper.
  4. chown -R alice: on a symlink — by default chown -R follows symlinks, which can change ownership outside the intended tree. Use chown -RH (follow only command-line symlinks) or --no-dereference (-h) explicitly.
  5. Web server can't read your files — check that every directory in the path has x for the web user, not just read on the final file.
  6. umask set too lateumask set in ~/.profile may not run for non-login shells. Use ~/.bashrc (bash) or ~/.zshrc (zsh) for guaranteed coverage.
  7. ACL mask demoting permissions — granting u:bob:rwx looks fine in setfacl -m but getfacl shows an effective: line if the mask is more restrictive. Raise the mask explicitly with setfacl -m m::rwx.
  8. Numeric UIDs in containers — when copying files between host and container, the same UID may belong to different users on each side. Use chown $(id -u):$(id -g) rather than hardcoding alice:alice for portability.
  9. setfacl silently widening your mask — every -m recomputes the mask. Tightening with m::r and then granting a fresh u:alice:rwx blows the mask back up to rwx. Use setfacl -n -m … or set the mask last to keep an explicit ceiling.
  10. setfacl/getfacl returning empty on NFS — those tools only speak POSIX ACLs. NFSv4 mounts need nfs4-acl-tools (nfs4_setfacl, nfs4_getfacl) and a different ACL grammar.
  11. cp stripping file capabilitiessecurity.capability is an xattr. Use cp -a / rsync -X to preserve it, or re-run setcap post-deploy. A binary with the cap on the source machine and no cap on the destination fails at runtime with EACCES, not a build error.
  12. Setuid root where a capability would dochmod u+s /usr/local/bin/mytool hands the binary every root power; setcap cap_net_bind_service=+ep hands it exactly one. Prefer capabilities (or systemd AmbientCapabilities=) for new code.

Real-world recipes

Make a directory writable only by my group

A common ask: alice and bob both belong to devs, and you want a directory where any group member can create files, and every new file is automatically owned by the devs group.

bash
sudo mkdir /srv/team-share
sudo chown alice:devs /srv/team-share
sudo chmod 2775 /srv/team-share          # owner+group rwx, others r-x, setgid
ls -ld /srv/team-share

Output:

text
drwxrwsr-x 2 alice devs 4096 May 24 10:00 /srv/team-share

The s in rws (group execute) marks it as setgid — new files inherit devs as their group regardless of the creator's primary group.

Fix "Permission denied" for nginx on a static site

If nginx returns 403 for files in your home directory, almost every time the problem is that one of the parent directories lacks x for the www-data user.

bash
# Diagnose: walk the path checking each directory
namei -l /home/alice/sites/blog/index.html

Output:

text
f: /home/alice/sites/blog/index.html
drwxr-xr-x root  root  /
drwxr-xr-x root  root  home
drwx------ alice staff alice                  <-- problem
drwxr-xr-x alice staff sites
drwxr-xr-x alice staff blog
-rw-r--r-- alice staff index.html
bash
# Fix: give group (or other) traversal on the home dir
chmod o+x /home/alice
# Or, more conservatively, add an ACL for nginx only:
sudo setfacl -m u:www-data:x /home/alice

Output: (none — exits 0 on success)

Audit SUID binaries

SUID binaries are a privilege-escalation surface. List them periodically and reconcile against your expected set.

bash
sudo find / -xdev -perm -4000 -type f -printf "%M %u %g %p\n" 2>/dev/null

Output:

text
-rwsr-xr-x root root /usr/bin/passwd
-rwsr-xr-x root root /usr/bin/sudo
-rwsr-xr-x root root /usr/bin/chsh
-rwsr-xr-x root root /usr/bin/mount
-rwsr-xr-x root root /usr/bin/su

Apply mode 644 to files and 755 to directories recursively

A one-liner that fixes the most common mistake — using chmod -R 644 and then losing directory traversal.

bash
find /var/www/site -type f -exec chmod 644 {} +
find /var/www/site -type d -exec chmod 755 {} +

# Or, in a single chmod with capital X:
chmod -R u=rwX,go=rX /var/www/site

Output: (none — exits 0 on success)

Strip every extended ACL from a tree

When a + mark on ls -l is showing up everywhere and you want to return to plain POSIX permissions.

bash
# Recursively remove all non-base ACL entries
sudo setfacl -R -b /srv/team-share

Output: (none — exits 0 on success)

Lock down a file root cannot accidentally delete

For protected config files (e.g. /etc/resolv.conf on systems where another process keeps rewriting it), set the immutable attribute.

bash
sudo chattr +i /etc/resolv.conf
# Confirm
lsattr /etc/resolv.conf
# Reverse later
sudo chattr -i /etc/resolv.conf

Output: (none — exits 0 on success)

stat -c '%a %n' FILE prints the permission mode in pure octal with no decoration — perfect for scripts that need to check or compare modes.

[!WARN] chmod 777 -R "fixes" almost everything for thirty seconds and creates a security incident an hour later. If you can't see why a path is failing, use namei -l to walk the access chain instead of opening everything to the world.

Bind to port 80 as a non-root service

Two paths to the same outcome — pick the systemd one for anything managed as a service.

bash
# Path A: file capability (works for any launcher — cron, manual run, etc.)
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myserver
sudo -u myapp /usr/local/bin/myserver        # binds to :80 without root

# Path B: systemd ambient capability (preferred for services)
sudo tee /etc/systemd/system/myserver.service >/dev/null <<'EOF'
[Service]
User=myapp
ExecStart=/usr/local/bin/myserver
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now myserver

Output: (none — exits 0 on success)

Audit all file capabilities on a host

File caps are an alternative privilege-escalation surface to SUID — audit them the same way.

bash
sudo getcap -r / 2>/dev/null

Output:

text
/usr/bin/ping cap_net_raw=ep
/usr/bin/mtr-packet cap_net_raw=ep
/usr/local/bin/myserver cap_net_bind_service=ep

Sources