Most Linux privilege escalation and data exposure starts with a file that has the wrong permissions: a world-writable script run by root, a setuid binary nobody needs, or a private key readable by every account on the host. Linux decides access with three layers that interact in ways that surprise even experienced administrators: the classic owner, group and other mode bits, the special setuid, setgid and sticky bits, and POSIX access control lists (ACLs) with their mask. This guide explains how each layer works according to the kernel and coreutils documentation, why chmod 777 is almost never the answer, how to audit setuid and world-writable files, how to fix permissions without breaking services, and what to monitor. It is for Linux administrators, platform engineers and security teams hardening servers, containers and build hosts.
Key takeaways
- Mode bits are the baseline. Owner, group and other each get read, write and execute, set with octal (
0640) or symbolic (u=rw,g=r,o=) modes; new files start from the requested mode with the umask removed, usually022. - Setuid is a privilege grant. A setuid-root executable runs with root's effective user ID for whoever starts it, so every one is part of your attack surface and should be justified, patched and inventoried.
- ACLs change the meaning of the group bits. Once an ACL has a mask,
chmod g=...changes the mask, not the owning group's entry, which silently widens or narrows access for every named user and group. chmod 777hides a design problem. Fix the owner, group or service account instead of letting every local account and compromised process write to the file.- Prevent, do not just find. Mount data and temporary filesystems
nosuid, setNoNewPrivileges=andRestrictSUIDSGID=on systemd services, and alert on permission changes with auditd.
How Linux permission checks work
Every file has an owner (UID), a group (GID) and a 12-bit mode. The kernel checks permissions against the process's effective user and group IDs and its supplementary groups, in a fixed order: if the process owns the file, only the owner bits apply; if not, and it is in the file's group, only the group bits apply; otherwise the other bits apply. The first matching class decides, which is why a file with mode 0604 can deny its own group read access while allowing everyone else.
| Bit | On a regular file | On a directory |
|---|---|---|
| r (4) | Read the contents | List the names in the directory |
| w (2) | Change the contents | Create, delete and rename entries (with x), regardless of the files' own permissions |
| x (1) | Execute as a program | Enter the directory and reach files inside it by name |
| setuid (4000) | Program runs with the file owner's effective UID | No standard effect on Linux; chmod keeps it unless explicitly cleared |
| setgid (2000) | Program runs with the file group's effective GID; without group execute it marks mandatory locking | New files inherit the directory's group, and new subdirectories inherit setgid |
| sticky (1000) | No effect on modern Linux | Only the file's owner, the directory's owner or a privileged process can delete or rename entries (as on /tmp) |
The directory write bit is the one most often misunderstood: write on a directory lets a user delete or replace a file they cannot read or write. A root-owned script with mode 0755 inside a directory writable by a service account can be replaced by that account. Always check the whole path, not only the file.
Reading modes and changing them safely
- Octal modes: one to four digits; the optional first digit sets setuid (4), setgid (2) and sticky (1), and the last three set owner, group and other.
0640is owner read and write, group read, other nothing. - Symbolic modes:
u,g,oandawith+,-or=. CapitalXadds execute only to directories and files that already have execute for someone, which makeschmod -R u=rwX,g=rX,o=safe for trees that mix scripts and data. - Clearing setuid and setgid on directories: GNU
chmodkeeps those bits on directories unless you clear them explicitly, for example with a leading zero (00755), a leading minus (-6000) or a leading equals sign (=755). - Symbolic links:
chmodnever changes a symlink's own mode; a link named on the command line changes its target, and recursive runs ignore links by default. - Recursive changes: GNU
chmoddefaults to--no-preserve-root; add--preserve-rootin scripts and aliases so a mistyped path cannot recurse over/.
umask: the permissions files are born with
Programs request a mode when they create a file, commonly 0666 for files and 0777 for directories, and the kernel clears any bits set in the process umask. With the usual 022, a requested 0666 becomes 0644. systemd sets UMask=0022 by default for system units. For services that write secrets, logs or uploads, set UMask=0077 or 0027 in the unit so new files are private by default. Two details matter: if the parent directory has a default ACL, the umask is ignored and the default ACL decides; and since Linux 4.7 the current umask is readable in the Umask field of /proc/<pid>/status, which is useful when auditing running services.
Setuid and setgid binaries
When a process runs a program with the setuid bit set, execve(2) changes the process's effective UID to the file's owner. A setuid-root program therefore runs as root no matter who starts it, and any bug in it, or in how it handles its environment, arguments and file descriptors, is a local root bug. The same page documents when the bits are ignored, which is the basis for most defences:
- nosuid mounts: the bits have no effect on a filesystem mounted
nosuid(some kernels return EPERM instead of ignoring them). - no_new_privs: the bits, and file capabilities, have no effect when the calling thread has the
no_new_privsattribute, which systemd sets withNoNewPrivileges=yes. - Tracing: the bits are ignored when the process is being ptraced.
- Scripts: Linux ignores setuid and setgid on interpreter scripts, so a setuid shell script is not a way to delegate privilege. Use sudo rules or a small, reviewed program instead.
The risk is concrete. CVE-2021-4034 ("PwnKit") was a memory corruption flaw in polkit's pkexec, which is setuid-root and installed by default on major distributions; Qualys reported it had existed since pkexec was created in 2009 and let any unprivileged local user gain root. It is in the CISA Known Exploited Vulnerabilities catalog with known ransomware use. Qualys's temporary mitigation, until a patched package was installed, was to remove the setuid bit (chmod 0755 /usr/bin/pkexec). Sudo's heap overflow CVE-2021-3156 is also in the KEV catalog. Each setuid binary you remove is one fewer place where the next flaw of that kind matters.
Auditing the setuid inventory
- List them:
find / -xdev -type f -perm -4000for setuid and-perm -2000for setgid, run per filesystem, and record path, owner, package and hash. - Map each to a package:
rpm -qfordpkg -S. A setuid file owned by no package, or in a home, web or temporary directory, is an incident until proven otherwise. - Decide per binary: keep it (needed, patched), remove the bit (feature unused, for example
pkexecon a server with no polkit use) or replace it with a capability (for examplecap_net_rawforping, where the distribution supports it). See the Linux Capabilities Guide. - Make the decision stick: package updates can restore a removed bit. Enforce the expected list with configuration management or a compliance profile and re-check after updates. See the OpenSCAP and Linux Compliance Scanning Guide.
- Baseline and diff: compare the list against the golden image weekly; any new setuid file outside a package update is an alert.
POSIX ACLs and the mask
ACLs (managed with getfacl and setfacl) let you grant access to named users and groups beyond the single owning group. The acl(5) manual defines six entry types: the owner (ACL_USER_OBJ), named users (ACL_USER), the owning group (ACL_GROUP_OBJ), named groups (ACL_GROUP), the mask (ACL_MASK) and other (ACL_OTHER). An ACL with any named entries must have exactly one mask.
| ACL behaviour | What it means in practice | Common mistake |
|---|---|---|
| The mask caps named users, the owning group and named groups | Effective rights are the entry ANDed with the mask; getfacl prints #effective: where they differ | Granting rw to a named user and wondering why writes fail |
| The mask does not cap the owner or other | Other access is still governed by the other entry and mode bits | Assuming an ACL makes a world-readable file private |
The group bits shown by ls -l are the mask when one exists | chmod g-w removes write from every named user and group | A routine chmod 640 cutting off a backup service account |
| Default ACLs on directories are inherited by new files and subdirectories | New objects take the default ACL, trimmed by the creating program's requested mode; the umask is ignored | Setting an access ACL on a directory and expecting new files to inherit it |
| First matching class decides | An owner match never falls through to named entries | Using a named user entry to restrict the file's owner |
| Deny-by-group is unreliable | The manual warns that unprivileged processes in user namespaces can drop supplementary groups with setgroups(2) | Using an ACL that removes rights from a group as a security boundary |
A + after the mode in ls -l output (-rw-r-----+) is the only hint that an ACL exists, which is why ACL-granted access is often missed in reviews. Include getfacl -R output in any permission audit of sensitive paths, and make sure backup and restore tools preserve ACLs (for example tar --acls or rsync -A), otherwise a restore silently changes who has access.
Choosing the right control
| Need | Use | Avoid |
|---|---|---|
| One service writes, one team reads | Owner = service account, group = team, mode 0640 | 0666 or 0777 |
| Several teams need different access to one tree | Named group ACL entries plus a default ACL on the directory | Adding everyone to one shared group |
| Shared drop directory | setgid directory with sticky bit (3770) and a default ACL | World-writable directory without sticky bit |
| A program needs one root power (bind a low port, raw sockets) | File or ambient capability for that one power | setuid root |
| An administrator needs to run a command as root | A specific sudo rule, logged | A setuid copy of a shell or interpreter |
| A file must not change even for root processes | Immutable attribute (chattr +i) plus file integrity monitoring | Relying on mode bits, which root bypasses |
| Confine what a root process can touch | SELinux or AppArmor policy; systemd ProtectSystem= and ReadOnlyPaths= | Discretionary permissions alone |
Discretionary permissions do not constrain root. Where a compromised root process is in your threat model, mandatory access control is the next layer. See the Reference Monitors and Mandatory Access Control Guide.
- Small estates: a short list of approved setuid binaries,
nosuidon data mounts and a weeklyfinddiff covers most of the risk. - Large estates: enforce permissions with configuration management, scan images in the build pipeline, and feed auditd
perm_modevents into the SIEM.
Why chmod 777 is the wrong fix
chmod 777 usually appears when an application cannot write a file and someone wants the error to go away. It grants read, write and execute to every account on the host, including every service account and anything that compromises one of them. On a directory it lets any user delete or replace every file inside. On an executable run by root or by a scheduled job it is a privilege escalation path.
- Find the real writer: check which user the process runs as (
ps -o user,pid,cmd, orUser=in the systemd unit). - Give that account ownership or group access:
chown app:appand mode0750for directories or0640for files, or a named ACL entry if several accounts need access. - Check the parent directories: the process needs execute on every directory in the path and write on the directory where it creates files.
- Check MAC denials: a "permission denied" with correct mode bits is often SELinux or AppArmor; look in the audit log for AVC or apparmor denials rather than widening the mode.
- Fix the creation path: if files keep appearing with the wrong mode, fix the service's
UMask=or the directory's default ACL, not each file.
Where identity fits
File permissions are how Linux protects the credentials that non-human identities use: SSH private keys, Kerberos keytabs, cloud credential files, API tokens in environment files and TLS private keys. A readable keytab or .aws/credentials file is a stolen identity. OpenSSH defends its own files: with StrictModes yes (the default), sshd checks the modes and ownership of the user's files and home directory before accepting a login, because world-writable files are a common novice mistake.
- Private keys and keytabs: owner-only (
0600or0400), owned by the service account that uses them, in a directory that is not group or world writable. See the Linux Service Accounts and Keytab Management Guide. - Service accounts: give each service its own account rather than sharing one, so permissions and audit records point to one workload.
- Environment and config files with secrets:
0640at most, group limited to the service, and better still moved to a secrets manager.
Detection and measurement
- auditd permission changes: the audit-userspace project's sample STIG rules record
chmod,fchmod,fchmodat,chownfamily and extended attribute calls (setxattr,removexattrand variants, which is how ACLs and file capabilities are written) by interactive users under the keyperm_mod. Alert on a new setuid or setgid bit, on world-write being added, and on any change under/etc,/usror credential directories. See the Linux Logging and auditd for Defenders Guide. - File integrity monitoring: track mode, owner, ACL and hash of setuid binaries, sudoers files and key files. See the Linux File Integrity Monitoring Guide.
- Scheduled scans: world-writable files (
-perm -0002 -type f), world-writable directories without the sticky bit, files with no valid owner (-nouser,-nogroup) and setuid files, diffed against the baseline.
| Metric | What it shows | Target |
|---|---|---|
| Setuid and setgid files per host versus approved list | Attack surface | No unapproved entries; list shrinking |
World-writable files outside /tmp-style directories | Unsafe sharing | Zero |
Writable mounts without nosuid (/tmp, /var/tmp, /home, data volumes) | Preventive coverage | Zero |
systemd services without NoNewPrivileges=yes | Privilege containment | Falling; exceptions documented |
| Credential files readable by group or other | NHI credential exposure | Zero |
Unexpected perm_mod events per week | Drift and possible tampering | Each one explained |
Implementation and rollback
- Measure first: run the scans above on a representative set of hosts and images; do not change anything yet.
- Agree an approved setuid list per build: start from the distribution default and remove what is unused.
- Pilot in a test ring: remove setuid bits, add
nosuidto/tmp,/var/tmp,/dev/shmand data mounts, setNoNewPrivileges=yesandRestrictSUIDSGID=yeson services, and run functional tests. Watch for tools that genuinely need setuid, such aspasswd,suandsudo. - Roll out by ring with a recorded rollback: keep the previous mode, owner and ACL of every file you change (
getfacl -Routput restores withsetfacl --restore), and keep mount changes in version-controlled configuration. - Enforce and monitor: configuration management restores the approved state after package updates; auditd and FIM catch drift.
Common failure patterns
- chmod 777 left in place: a quick fix during an outage becomes permanent, and a web upload directory or cron script becomes writable by every account.
- Writable directory, protected file: the file is root-owned
0755, but its directory is writable by a service account that can replace it. - Setuid bits restored by updates: a mitigation such as removing the bit from
pkexecis undone by the next package update and nobody notices. - Invisible ACLs: reviews read
ls -l, miss the+and never see the named user entry that grants a contractor account write access. - Restores that drop or change ACLs: a backup tool without ACL support restores files with only mode bits, removing or widening access.
- Credential files with default modes: a deployment script writes a keytab or token file with the default
0644because the service's umask was never set.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | AC-3 Access Enforcement; AC-6 Least Privilege; CM-6 Configuration Settings; CM-7 Least Functionality; SI-7 Software, Firmware, and Information Integrity | Correct modes and ACLs, minimal setuid, enforced baselines, integrity monitoring |
| NIST CSF 2.0 | PR.AA-05; PR.PS-01 | Least-privilege permissions; configuration management |
| MITRE ATT&CK v20 | T1548.001 Setuid and Setgid; T1222.002 Linux and Mac Permissions | Abuse of setuid binaries; permission changes to enable execution or persistence |
| MITRE D3FEND | D3-FAPA File Access Pattern Analysis | Detecting unusual access to key and credential files |
| CISA KEV | CVE-2021-4034 (polkit pkexec); CVE-2021-3156 (sudo) | Exploited flaws in setuid-root programs |
Practitioner checklist
- Inventory setuid and setgid files on every build and image, map each to a package and approve or remove it.
- Mount
/tmp,/var/tmp,/dev/shm, home and data volumes withnosuid(andnodev, andnoexecwhere workloads allow). - Set
NoNewPrivileges=yes,RestrictSUIDSGID=yesand a restrictiveUMask=on systemd services. - Replace setuid-root with capabilities or specific sudo rules where possible.
- Remove world-write from everything except sticky temporary directories, and ban
chmod 777in runbooks and code review. - Include
getfacloutput in permission reviews and confirm backups preserve ACLs. - Make SSH keys, keytabs and credential files owner-only and owned by a dedicated service account.
- Deploy auditd
perm_modrules and file integrity monitoring on setuid binaries and credential paths, and alert on new setuid files. - Re-run the baseline after every package update and image rebuild.
Standards and references
- Linux man-pages: inode(7) file type and mode
- Linux man-pages: execve(2)
- Linux man-pages: acl(5) Access Control Lists
- Linux man-pages: chmod(1)
- Linux man-pages: umask(2)
- systemd: systemd.exec execution environment configuration
- Qualys: PwnKit local privilege escalation in polkit's pkexec (CVE-2021-4034)
- CISA: Known Exploited Vulnerabilities Catalog
Related NHI Mgmt Group resources: Linux Capabilities Guide · Linux Privilege Escalation Techniques Guide · Linux Fundamentals for Defenders Guide · Linux Server Hardening Guide · Linux Service Accounts and Keytab Management Guide