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".
ls -l /home/alice
Output:
-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
| Character | Meaning |
|---|---|
- (position 1) | regular file |
d | directory |
l | symbolic link |
c / b | character / block device |
s / p | socket / 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.
| Bit | On a file | On a directory |
|---|---|---|
r | Read contents | List entries (i.e. ls) |
w | Modify contents | Create, rename, delete entries |
x | Execute as program | Traverse (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.
# 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.
| Octal | Permissions | Common use |
|---|---|---|
0 | --- | no access |
1 | --x | execute / traverse only |
4 | r-- | read only |
5 | r-x | read + execute (executables, public dirs) |
6 | rw- | read + write (data files) |
7 | rwx | full access |
Common combinations:
| Mode | Means |
|---|---|
644 | owner rw, others r — text files |
600 | owner rw only — secrets, SSH keys |
755 | owner rwx, others rx — scripts, public dirs |
700 | owner rwx only — private dirs |
750 | owner full, group rx, others none — group-shared scripts |
775 | owner+group full, others rx — group-writable web roots |
2775 | as above plus setgid — group inheritance (see below) |
1777 | rwx for all, sticky bit — /tmp |
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:
WHOisu(user/owner),g(group),o(other), ora(all = ugo).OPERATORis+(add),-(remove), or=(set exactly).PERMSis any combination ofr,w,x(and the special bitss,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.
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 lowercasex) when recursing withchmod -R. Lowercasexmakes every file executable, including text and images. CapitalXonly 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.
| Bit | Symbol | Octal | Effect |
|---|---|---|---|
| setuid | s in user-x | 4000 | On executable: process runs as the file's owner (not the invoker). |
| setgid | s in group-x | 2000 | On executable: process runs as the file's group. On directory: new entries inherit the directory's group. |
| sticky | t in other-x | 1000 | On directory: only the file's owner (or root) can rename / delete entries. |
ls -l /usr/bin/passwd /tmp
Output:
-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.
# 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
sudowith 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.
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).
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.
| umask | New file mode | New dir mode | Typical for |
|---|---|---|---|
022 | 644 | 755 | most distros' default |
002 | 664 | 775 | shared group workflow |
077 | 600 | 700 | single-user, strict |
027 | 640 | 750 | server processes |
umask # show current value
umask 077 # strict — only owner can read new files
touch private.txt
ls -l private.txt
Output:
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.
# ~/.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.
# 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:
# file: shared.log
# owner: alice
# group: devs
user::rw-
user:bob:rw-
group::r--
mask::rw-
other::---
# 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
maskis an upper bound on what any named user or group can be granted. If you setu:bob:rwxbut the mask isrw, bob effectively hasrw.setfacl -m m::rwx fileraises the mask.
[!WARN] Mask auto-recalculation surprises. Every
setfacl -mrecomputes the mask as the union of named-user / named-group / owning-group permissions — so a mask you explicitly tightened withm::rwill silently widen back torwxthe next time you grantu: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.
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).
sudo chattr +i /etc/resolv.conf # make it immutable
lsattr /etc/resolv.conf
Output:
----i--------e----- /etc/resolv.conf
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
lsattrfirst. Aniflag from a previouschattr +iis the usual culprit; remove it withchattr -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.
# 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:
/usr/local/bin/myserver cap_net_bind_service=ep
# 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.
| Set | Letter | Where it lives | Purpose |
|---|---|---|---|
| Permitted | p | process + file | The capabilities a process may exercise — the upper bound. |
| Effective | e | process + 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". |
| Inheritable | i | process + file | Capabilities that pass to a child only if the child's file capabilities request them. |
| Bounding | — | process | A per-process ceiling — capabilities outside it can never be re-acquired, even via setuid-root. |
| Ambient | — | process (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.
[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 survivecp -aandrsync -X, but a plaincpor 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-runsetcapafter deploys, or verify withgetcapin CI.
Common pitfalls
- Forgetting
xon directories —chmod 644 -R project/strips thexfrom directories and you can no longercdinto them. Use capitalX(chmod -R u=rwX,go=rX project/) or apply different modes to files and directories separately. - SSH key with
644—sshdrefuses to use a private key that is group- or world-readable.chmod 600 ~/.ssh/id_*andchmod 700 ~/.ssh. - Setuid on a script — has no effect; the kernel ignores it for interpreted files. Use
sudoor compile a small C wrapper. chown -R alice:on a symlink — by defaultchown -Rfollows symlinks, which can change ownership outside the intended tree. Usechown -RH(follow only command-line symlinks) or--no-dereference(-h) explicitly.- Web server can't read your files — check that every directory in the path has
xfor the web user, not just read on the final file. umaskset too late —umaskset in~/.profilemay not run for non-login shells. Use~/.bashrc(bash) or~/.zshrc(zsh) for guaranteed coverage.- ACL mask demoting permissions — granting
u:bob:rwxlooks fine insetfacl -mbutgetfaclshows aneffective:line if the mask is more restrictive. Raise the mask explicitly withsetfacl -m m::rwx. - 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 hardcodingalice:alicefor portability. setfaclsilently widening your mask — every-mrecomputes the mask. Tightening withm::rand then granting a freshu:alice:rwxblows the mask back up torwx. Usesetfacl -n -m …or set the mask last to keep an explicit ceiling.setfacl/getfaclreturning empty on NFS — those tools only speak POSIX ACLs. NFSv4 mounts neednfs4-acl-tools(nfs4_setfacl,nfs4_getfacl) and a different ACL grammar.cpstripping file capabilities —security.capabilityis an xattr. Usecp -a/rsync -Xto preserve it, or re-runsetcappost-deploy. A binary with the cap on the source machine and no cap on the destination fails at runtime withEACCES, not a build error.- Setuid root where a capability would do —
chmod u+s /usr/local/bin/mytoolhands the binary every root power;setcap cap_net_bind_service=+ephands it exactly one. Prefer capabilities (or systemdAmbientCapabilities=) 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.
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:
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.
# Diagnose: walk the path checking each directory
namei -l /home/alice/sites/blog/index.html
Output:
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
# 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.
sudo find / -xdev -perm -4000 -type f -printf "%M %u %g %p\n" 2>/dev/null
Output:
-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.
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.
# 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.
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' FILEprints 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, usenamei -lto 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.
# 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.
sudo getcap -r / 2>/dev/null
Output:
/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
- capabilities(7) — Linux manual page
- setfacl(1) — Linux manual page
- systemd.exec — AmbientCapabilities / CapabilityBoundingSet
- nfs4_setfacl(1) — Linux manual page
- Red Hat: Why filesystem ACL commands like setfacl/getfacl don't work on NFSv4
- Inheriting capabilities — LWN
- Linux Capabilities In Practice — Container Solutions
- Stop using setuid for everything: practical Linux file capabilities with getcap, setcap, and systemd — Dev|Journal (2026)
- How to use the ACL mask to limit effective permissions on RHEL — OneUptime (2026)
- Linux setfacl Command: ACL Permissions Guide with Examples (2026) — getpagespeed