Linux Discretionary Access Control: Mode Bits, SUID/SGID/Sticky, umask, POSIX ACLs & Capabilities
Executive Summary: Executing
chmod -R 777to "fix" a permission error destroys multi-tenant security boundaries and causes hardened daemons likesshdandsendmailto refuse startup immediately. This architectural reference dissects Linux Discretionary Access Control (DAC): file vs. directory bit semantics, special high-order bits (SUID4000, SGID2000, Sticky1000), processumaskbitmask subtraction, fine-grained POSIX Access Control Lists (setfacl/getfacl), ext4/XFS immutable attributes (chattr +i), and replacing root SUID binaries with Linux Kernel Capabilities (setcap).
-
File vs. Directory Bit Semantics
Understand why
x(Execute) on a directory (search/traversebit) is required tostat()orcdinto Inodes inside that directory. -
SUID, SGID & Sticky Bit (
1777)
Master group-inheritance directories (
chmod 2770) for shared team repositories and deletion-restricted Sticky directories (/tmp). -
POSIX Extended ACLs (
setfacl)
Grant granular read/write access to specific service accounts (
www-data,postgres) without altering primary UID/GID ownership. -
Kernel Capabilities (
setcap) &chattr
Grant isolated privileges like
cap_net_bind_serviceorcap_net_rawto unprivileged binaries and lock critical files withchattr +i.
1. Architecture: How the Kernel Evaluates DAC Permissions
When a process (carrying an Effective UID euid, Effective GID egid, and supplementary groups groups) invokes the open(2) or execve(2) syscall on a path, the Linux kernel evaluates permissions in strict short-circuit order—it does not OR the User, Group, and Other permissions together:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TD
Syscall["Process Calls open('/srv/app/config.env')"] --> Traverse{"1. Does Process Have 'x' (Traverse)<br/>on /, /srv, and /srv/app?"}
Traverse -->|"No"| EACCES1["Return -EACCES (Permission Denied)"]
Traverse -->|"Yes"| MatchUID{"2. Does Process euid == File UID?"}
MatchUID -->|"Yes"| EvalUser["Evaluate ONLY User Bits (rwx------)<br/>(Short-Circuit: Group/Other Ignored!)"]
MatchUID -->|"No"| MatchGID{"3. Is Process egid/groups in<br/>File GID or POSIX ACL?"}
MatchGID -->|"Yes"| EvalGroup["Evaluate Group Bits / ACL Mask"]
MatchGID -->|"No"| EvalOther["Evaluate Other Bits (------rwx)"]
The Crucial Difference Between File Bits and Directory Bits
| Permission Bit | Octal Value | Meaning on a Regular File (-) |
Meaning on a Directory (d) |
|---|---|---|---|
r (Read) |
4 (100₂) |
Read the data blocks of the file (open with O_RDONLY). |
Read the list of filenames (readdir / ls) stored in the directory block. |
w (Write) |
2 (010₂) |
Modify or truncate the file's data blocks (O_WRONLY / O_RDWR). |
Create, rename, or delete (unlink) files inside the directory (requires x bit as well!). |
x (Execute) |
1 (001₂) |
Execute the file as a program (execve). |
Traverse / Search (lookup): Access Inode metadata (stat) or cd into the directory. |
Why a User Can Delete a Root-Owned 0400 File Inside a Group-Writable Directory
Remember from VFS architecture: deleting (unlink) or renaming a file modifies the parent directory's entry table, not the file's own data blocks! If /srv/shared has permissions drwxrwxr-x (0775) and contains a file owned by root:root with mode 0400 (-r--------), any user with write (w + x) permission on /srv/shared can rm that root-owned file unless the Sticky Bit (1000 / +t) is set on /srv/shared!
2. Special Mode Bits (SUID, SGID, Sticky) & Process umask
Above the standard 9-bit rwxrwxrwx triad sit 3 high-order special bits (4000, 2000, 1000):
| Special Bit | Octal | Symbolic | Effect on Executable Files | Effect on Directories |
|---|---|---|---|---|
| SUID | 4000 |
u+s (rws) |
Process runs with Effective UID of the file owner (e.g., /usr/bin/passwd, /usr/bin/sudo). |
Ignored on Linux. |
| SGID | 2000 |
g+s (rws) |
Process runs with Effective GID of the file's group. | New files & subdirectories created inside automatically inherit the parent directory's GID instead of the creator's primary group! |
| Sticky | 1000 |
+t (rwt) |
Ignored on modern Linux. | Deletion Restriction: Only the file's owner, the directory's owner, or root can delete or rename files inside (e.g., /tmp is 1777). |
(Note: An uppercase S or T in ls -l—such as rwS--- or rwt vs rwT—indicates that the special bit is set, but the underlying execute (x) bit is missing.)
# 1. Create a shared engineering directory where all new files inherit the 'devops' group (SGID 2770)
sudo groupadd -f devops
sudo install -d -m 2770 -o root -g devops /srv/devops-repo
# 2. Use symbolic capital '+X' to grant execute ONLY on directories (and already-executable scripts),
# never accidentally making plain text/config files executable!
sudo chmod -R u=rwX,g=rwX,o= /srv/devops-repo
# 3. Set a strict default umask (0027) in /etc/profile.d/umask.sh:
# Default File Base (0666) & ~0027 = 0640 (-rw-r-----)
# Default Dir Base (0777) & ~0027 = 0750 (drwxr-x---)
umask 0027
3. Fine-Grained Access Control: POSIX ACLs (setfacl & getfacl)
When a web server (www-data) needs read-only access to logs written by an application daemon (appuser:appgroup), adding www-data to appgroup might grant excessive access to other files owned by appgroup. Instead, apply a targeted POSIX Access Control List (ACL) together with a Default ACL (-d) so future rotated files automatically inherit the rule:
# 1. Grant user 'www-data' read/traverse access to existing files and directories in /var/log/custom-app
sudo setfacl -R -m u:www-data:rX /var/log/custom-app
# 2. Set a Default ACL (-d) on the directory so newly created log files automatically grant www-data r-x
sudo setfacl -R -d -m u:www-data:rX /var/log/custom-app
# 3. Inspect active ACLs and the effective mask (notice the '+' sign in ls -ld /var/log/custom-app)
getfacl /var/log/custom-app
4. Linux Capabilities (setcap) & Immutable Ext4/XFS Flags (chattr)
Instead of setting the dangerous SUID-root bit (chmod 4755) on a custom Go or C binary just so it can bind port 443 or send ICMP echo requests, grant only the specific Linux Kernel Capability required:
# 1. Allow an unprivileged binary to bind ports < 1024 without running as root or SUID
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy
# 2. Verify capabilities attached to the binary's security.capability extended attribute
getcap /usr/local/bin/caddy
# 3. Make /etc/resolv.conf or backup directories immutable (+i) or append-only (+a) even against root!
sudo chattr +i /etc/resolv.conf
lsattr /etc/resolv.conf
# (Remove immutable flag before legitimate maintenance via: sudo chattr -i /etc/resolv.conf)