In October of 2021, the major streaming platform, Twitch, suffered one of the most severe breaches in its history – 125GB of internal data, including source code, creator payouts, and proprietary tooling, posted publicly to 4chan. The vector wasn’t a zero-day. It wasn’t a nation-state APT with custom malware. The breach resulted from a server misconfiguration that allowed public access to internal systems. A permission problem on a Linux server.
It’s a story that repeats itself more often than the industry likes to admit. Most attackers don’t need to break every control – they just need one gap, then pivot quietly from there. In Linux environments, that gap is almost always a permission misconfiguration that looks completely harmless when someone sets it.
Here’s the uncomfortable truth: Linux file permissions are deceptively simple on the surface and surprisingly dangerous underneath. Every security professional knows that chmod 755 does. Far fewer can tell you what happens when CAP_DAC_OVERRIDE lands on a binary, why a missing sticky bit on /tmp is an escalation vector waiting to happen, or how ACL entry silently overrides everything ls -l is telling you. In real-world attacks, permission misconfigurations frequently serve as an initial foothold or a privilege-escalation vector. The gap between “I know permissions” and “I can audit permissions like an attack” is exactly where breaches live.
This linux file permissions cheat sheet is built for practitioners who are past the basics. If you need to go back and learn some essentials be sure to see our linux cheat sheet Whether you’re enumerating a target during a pentest, hardening a production Linux host, or investigating a post-incident timeline, this is the reference you’ll want open. We cover the full stack – standard DAC bits, SUID/SGID/sticky, ACLs, Linux capabilities, mandatory access control, and the audit commands that tie it all together – with a security-first lens on every single one.
Bookmark it. You’re going to use it.
Special Permission Bits: SUID, SGID, and Sticky
If standard Unix permissions are the lock on the front door, special permission bits are the skeleton key hanging quietly on the wall – usually fine where the administrator left them, immediately dangerous the moment an attacker finds one that doesn’t belong.
SUID (Set User ID) – The Most Abused Bit on Linux
SUID causes a binary to execute with the permissions of the file’s owner rather than the user running it. On /usr/bin/passwd, that’s intentional – the binary needs root privileges to write to etc/shadow, and SUID is how the system grants that temporarily to any user who runs it. The problem is not SUID itself. The problem is SUID on binaries that were never designed to carry it.
Consider what happens when a developer installs Python and sets the SUID bit to make scripting easier across user accounts – a configuration choice that looks harmless in isolation. An attacker with any foothold on that machine runs a single find command, spots the non-standard binary, and within seconds they have root shell:
# Attacker's enumeration - first thing run after initial access
find / -perm -4000 -type f 2>/dev/null
# Output excerpt on a poorly hardened system:
# /usr/local/bin/python3-suid ← non-standard, immediately suspcious
# /usr/bin/passwd ← expected
# /usr/bin/sudo ← expected
# Exploitation takes one line
/usr/local/bin/python3-suid -c 'import os; os.setuid(0); os.system("/bin/bash")'
# root@target:~#
This is not a theoretical scenario. It mirrors the exact misconfiguration pattern documented in real-world penetration test engagements, where SUID on interpreter binaries (Python, Perl, Ruby) is one of the most reliable privilege escalation vectors found on production servers. Any interpreted language binary with SUID set should be treated as a critical finding – these tools offer too much control over system calls to safely carry root’s identity.
The GTFOBins factor makes this worse. The GTFOBins project (gtfobins.github.io) catalogs exploitation paths for dozens of common Unix binaries when they carry unexpected SUID. Text editors like vim, network tools like older versions of nmap, and standard utilities like find all have documented SUID exploitation paths. A non-standard SUID binary on any system you’re assessing should be your first cross-reference stop.
Hardening guidance: Audit SUID binaries on every new system and after software installation – packages installed to non-standard path like /opt/ or /usr/local/ are the most common source of unexpected SUID bits. Your baseline should be the known-good list for your distribution; anything not on it warrants investigation. Remove SUID with chmod u-s /path/to/binary and verify the binary functions correctly without it.
SGID (Set Group ID) – Privilege Escalation’s Quieter Cousin
SGID works identically to SUID but operates at the group level. On a binary, the process runs as the file’s group rather than the executing user’s group. On a directory, every file created inside inherits the directory’s group rather than the creating user’s primary group. The directory behavior is more the more common legitimate use case – shared project directories where a development team needs consistent group ownership on new files. The binary behavior is the attack surface. A root-owned binary with SGID grants the executing user effective group membership of whatever group owns the file – if that group has elevated access to log files, database sockets, or backup scripts, an attacker with SGID exploitation can reach those resources.
# Find SGID binaries
find / -perm -2000 -type f 2>/dev/null
# Find SGIT directories (check for unintended group inheritance)
find / -perm -2000 -type d 2>/dev/null
ls -al and verify the owning group still maps to the intended team. For binaries, apply the same scrutiny as SUID: if it’s not a known system binary with a documented reason for SGID, investigate it and remove it. Sticky Bit – The Guard on /tmp
The sticky bit on a directory restricts deletion: only the file owner, the directory owner, or root owner can remove or rename files inside it, regardless of the directory’s write permissions. This is why /tmp is 1777 – world-writable so any user can create temp files, but sticky so no user can delete another user’s files.
The attack surface is the absence of this bit. Without the sticky bit on a world-writable directory any user can delete or replace any file in that directory – including files created by other users or by privileged processes. Replace a script that a root-owned cron job writes to /tmp and reads back, and you have code execution as root.
# verify sticky on world-writable directories
ls -ld /tmp /var/tmp
# Expected: drwxrwxrwt ← the 't' is the sticky bit
# Find world-writable directories missing the sticky bit
find / -type d -perm -o+w ! -perm -1000 -not -path "/proc/*" 2>/dev/null
/tmp and /var/tmp should always be 1777. Beyond that, any shared directory used by automated processes – backup staging areas, upload directories, inter-process communication paths – should carry the sticky bit if multiple users or processes interact with it. Access Control Lists (ACLs): The Hidden Permissions
Standard Unix permissions give you three knobs: owner, group, others. That model served well for decades, but modern multi-user environments routinely need something more surgical – grant one specific contractor read access to a directory without touching the group assignment or restrict a single service account from a file that everyone else in its group should reach. That’s what ACLs are for.
The security problem with ACLs is not what they can do. It’s that they’re invisible to the tool most administrators and analysts instinctively reach for.
ls -l /etc/app-config
# -rw-r--r-- 1 root admin 1.2K Jun 10 09:22 /etc/app-config
That looks fine. The owner group has read-write, the admin group has read, others have read. Now run the tool that actually tells the truth:
getfacl /etc/app-config
# file: etc/app-config
# owner: root
# group: admin
# user::rw-
# user::contractor_dan:rw- ← this is invisible to ls -l
# group::r--
# mask::rw-
# other::r--
A named user ACL entry granting a contractor read-write access to a configuration file – completely invisible to standard permission inspection. During incident response or a routine audit, this kind of entry gets missed constantly. NSA and CISA have both flagged ACL misconfiguration on data shares and repositories as one of the top ten commonly exploited misconfigurations in enterprise environments, because the access is real and the visibility is near zero unless you’re specifically looking for it.
ACL Masks – The Ceiling Nobody Checks
Every ACL that includes named user or group entries also carries a mask entry. The mask is the effective permission ceiling for all named users and groups – it does not affect the file owner or the “others” class, but it caps everything in between. If a named user ACL grantsrwx but the mask is r--, the effective permission is r--. This can create a false sense of security when the mask is looser than intended, or cause confusing access denials when it’s tighter.
# Check effective permissions after mask calculation
getfacl /sensitive/dir
# Lower the mask to restrict all named ACL entries at once
setfacl -m mask::r-- /sensitive/dir
# Remove all ACL entries and return to standard permissions
setfacl -b /senstivie/file
Default ACls – Silent Inheritance
Default ACLs on directories are inherited automatically by every new file and subdirectory created inside. Set a default ACL once and walk away – every file created by any user in that directory tree inherits whatever permissions you defined, regardless of umask. This is powerful for consistent shared directory permissions and genuinely dangerous when the default ACL is too permissive and nobody is monitoring what gets created.
Hardening guidance: Build ACL auditing into your standard checklist. getfacl -R /path on any sensitive directory tree will surface named user entries, default entries, and mask values that ls -al will never show you. Strip ACLs that don’t have documented, current justification with setfacl -b. For shared directories that need default ACLs, document the expected entries and include them in change management so additions are noticed
Linux Capabilities: Surgical Privileges, Surgical Risk
For most of Linux’s history, privilege was binary: you were root (UID 0) and could do anything, or you weren’t and couldn’t. The capabilities system, introduced to give programs specific elevated privileges without full root, was a significant improvement in principle. In practice, it created a new category of privilege escalation vector that many administrators don’t audit at all.
Capabilities divide root’s powers into discrete units – CAP_NET_RAW for raw socket access, CAP_CHOWN for changing file ownership, CAP_SYS_ADMIN for mount operations and namespace management, and so on. A binary granted cap_net_raw+ep can open raw sockets without being SUID root. That’s cleaner than SUID – until someone assigns the wrong capability to the wrong binary.
The difference from SUID: capabilities don’t show up in ls -l. SUID at least leaves a visible s in the permission string. Capabilities are stored in extended attributes and are only visible through getcap.
# The single most important capability audit command
getcap -r / 2>/dev/null
Run that on a system you’re assessing and you may be surprised what comes back.
CAPT_SETUID – The Root Backdoor That Doesn’t Look Like One
CAP_SETUID allows a process to call setuid() and change its effective user ID to any value, including zero. Any binary holding this capability is functinoally a root escalation path for any user who can execute it.
The attack is identical to the SUID python example above – but even harder to catch because ls -l shows nothing unusual.
# Capability shows in getcap, not ls -l
getcap /usr/bin/python3
# /usr/bin/python3 = cap_setuid+ep
# Exploitation - same one-liner as SUID
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'
# root@target:~#
This misconfiguration appears in real assessment findings when administrators try to grant Python scripts elevated access to specific automation tasks without making the binary SUID – the capability feels more targeted, but the escalation path is identical.
CAP_DAC_OVERRIDE – When chmod Stops Mattering
CAP_DAC_OVERRIDE bypasses all discretionary access control checks. A process holding this capability can read, write, and execute any file on the system regardless of its permission bits. Every chmod 600 and chmod 000 you’ve carefully applied become irrelevant to any process carrying this capability.
# If a binary has cap_dac_override, it ignores all file permissions
# Check for it during any audit
getcap -r / 2>/dev/null | grep dac_override
CAP_SYS_ADMIN – The Container Escape Key
CAP_SYS_ADMIN is the broadest capability in the Linux kernel – it covers mounting filesystems, loading kernel modules, managing namespaces, setting hostnames, and a dozen other operations. It is near-equivalent to full root, and it is the primary container escape vector.
A containerized process with CAP_SYS_ADMIN can mount the host filesystem, manipulate namespaces to escape container isolation, and in several documented CVEs, achieve full host kernel access. CVE-2022-0492, a Linux kernel vulnerability affected cgroup release agents, required either root or CAP_DAC_OVERRIDE to exploit inside a container – containers running privileged or with CAP_SYS_ADMIN were fully exposed.
# Check capabilities of the current process
capsh --print
# Check capabilities on running container
cat /proc/1/status | grep Cap
# Decode capability bitmask
capsh --decode=0000003fffffffff
Hardening Capabilities
The principle here is simple but rarely applied consistently: no capability should be present without a documented, current operational requirement. Run getcap -r / after every software installation and deployment. Packages installed to non-standard paths frequently arrive with capabilities set, and those get forgotten.
Remove capabilities that aren’t needed:
# Remove all capabilities from a binary
setcap -r /path/to/binary
# Verify removal
getcap /path/to/binary
# (no output = no capabilities)
For containers, define a minimal capability set in your container runtime configuration and drop everything else with --cap-drop ALL --cap-add for only what the application actually requires.
Extended Attributes: The Hardening Layer Attackers Remove First
Extended file attributes sit beneath standard permissions and capabilities in most hardening conversations, which is precisely why they’re worth understanding well. The two most operationally relevant are+i (immutable) and +a (append-only), both set via chattr and visible only via lsattr.
The Immutable Flag (+i)
A file with the immutable attribute set cannot be modified, deleted, renamed, or hard-linked by anyone, including root – until the flag is explicitly removed. It’s the closest thing Linux has to a write-once protection at the filesystem level.
The catch – an attacker with root access can remove the flag just as easily as you set it:
This means the immutable flag is not a security control against root-level attackers. It’s a tripwire. Any attempt to modify a chattr +i file before removing the flag will fail with a permission error, generating a log entry. With auditd monitoring chattr system calls, you get an alert the moment an attacker tries to weaken your files – giving you detection coverage that the flag itself doesn’t provide
# Adutid rule to catch chattr being called on auth files
-a always,exit -F arch=b64 -S ioctl -F a1=0x40086601 -k chattr_detected
The Append-Only Flag (+a)
chattr +a allows a file to be opened only in append mode – new content can be written to the end, but existing content cannot be modified or truncated. The canonical use case is active log files: a compromise service or user cannot wipe the log that records their activity.
#
chattr +a /var/log/auth.log
chattr +a /var/log/syslog
# Verify
lsattr /var/log/auth.log
# -----a-------e-- /var/log/auth.log
Hardening guidance: Apply +i to files that should never change at runtime passwd, shadow, sudoers, and hosts under the etc directory; and SSH configuration files. Apply +a to active log files. Run lsattr on your critical file list as a regular audit step – the absence of expected flags after a security incident can be forensic evidence of root-level tampering.
Mandatory Access Control: The Layer That Overrides Everything
Discretionary Access Control (DAC) – the standard Unix permission model – is called “discretionary” for a reason: the file owner decides who gets access. MAC removes that discretion entirely and replaces it with system-wide policy that applies even to root.
Even a process running as UID 0 is subject to MAC policy. This is the fundamental shift: compromise of a user account, even root, does not automatically grant an attacker the ability to do anything – the MAC polixy defines what any process is allowed to do regardless of its identity.
AppArmor – Kali & Ubuntu’s Default MAC
AppArmor assigns a security profile to each confined process. The profile defines exactly which files the process can read, write, or execute which network operations it can perform; and what other system resources it can touch. Anything not explicitly permitted is denied and logged.
The two states you’ll encounter are what matter most to security practitioners:
Enforce mode is the operational state. Violations are blocked at the kernel level and written to syslog. This is the only acceptable state for any internal-facing service in production.
Complain mode logs violations but never blocks them. It’s used during profile development to capture what a process actually needs before writing the enforced rules. From a threat modeling perspective, a process in complaint mode should be treated as completely unconfined – an attacker can perform any operation the kernel would normally allow.
# Check all confined processes and their current mode
Aa-status
# Sample output:
# 42 profiles are loaded.
# 38 profiles are in enforce mode.
# /usr/bin/evince
# /usr/sbin/tcpdump
# 4 profiles are in complain mode. ← investigate these
# /usr/bin/custom-app ← never in complain on production
# Promote to enforce
aa-enforce /etc/apparmor.d/usr.bin.custom-app
# Read denial logs
grep "apparmor" /var/log/syslog | grep "DENIED"
aa-status after system updates on production hosts.
SELinux – Context-Based Mandatory Control
Where AppArmor works with file paths, SELinux works with security context – labels assigned to every file, process, port, and socket on the system. Policy rules define which context types may interact with which others. The context is visible withls -Z: That label – shadow_t – means only processes with policy permission to interact with shadow_t type objects can access the file, regardless of standard Unix permissions. A compromised web server process running as root but confined to the http_t type cannot read etc/shadow because the SELinux policy doesn’t permit http_t to access shadow_t objects.
The wrong context is a common misconfiguration that produces one of two failure modes: legitimate processes getting denied access (causing service disruption) or files carrying an overly permissive context that lets unintended processes reach them.
Hardening guidance for both AppArmor & SELinux: The goal is confined attack surface. Every internet-facing service should have a profile in enforce mode. New services should have profiles written during staging, not after production deployment. For SELinux, prefer restorecon over chcon for context fixes – chcon sets temporary lebel that can be overwritten by a relabelling operation, while restorecon applies the policy’s defined correct context persistently.
Mount Options & Audit Checks: The Systemic Hardening Layer
Individual file permissions protect individual files. Mount options and system-wide audit practices protect the environment those files live in – and they’re where many hardening guides stop short.
noexec and nosuid – Controlling /tmp
Two mount options belong on every world-writable filesystem, and their absence on [folder] is a finding on any hardening system assessment:
nosuid tells the kernel to ignore SUID and SGID bits on all files within that mount. Drop a SUID root binary into a /tmp mounted with nosuid and it runs as the user who executed it, not as root. This directly neutralizes the “drop a SUID binary and escalate” attack chain.
noexec prevents direct execution of binaries from the mount point. An attacker can’t execute a compiled binary they’ve uploaded to /tmp/evil.py. The important caveat: noexec does not stop interpreter-based attacks. python3 /tmp/evil.py will run because the kernel is executing python, not the script directly. noexec reduces attack surface but must be layered with MAC profiles for meaningful protection.
\# Verify current mount options
mount | grep /tmp
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)
# ↑ this is the hardened state
# Set permanently in etc/fstab
# tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
# Apply without rebooting
mount -o remount,noexec,nosuid /tmp
Umask – The Default Permission Floor
Umask is the subtraction mask applied to every newly created file and directory. The default on most Linux systems is 022, meaning new files are created as 644 (rw-r--r--) and directories as 755 (rwxr-xr-x) – world-readable by default.
For service accounts and sensitive environments, 022 is too permissive. A umask of 027 produces files at 640 (no world access) and directories at 750. A umask of 077 produces files at 600 and directories at 700 – no group or world access at all.
# Check current umask
umask
# 0022 ← common default; consider tightening for service accounts
# Tighten for the current session
umask -27
# Make permanent for all users
echo "umask 027" >> /etc/profile
# Verify the default in /etc/login.defs
grep UMASK /etc/login.defs
Orphaned Files – The UID Reclamation Attack
When a user account is deleted without removing their files, those files become “orphaned” – owned by a UID number that no longer maps to any account name. The attack is simple: create a new user with the same UID and you inherit ownership of every orphaned file that user left behind. On a system with poor housekeeping, this can include configuration files, scripts, or home directory content from an account that was deleted months ago.
# Find all orphaned files and directories
find / -nouser 2>/dev/null
find / -nogroup 2>/dev/null
Run both commands immediately after any user account deletion and either reassign orphaned files to root or delete them before the UID can be recycled.
World-Writable Files – The Most Dangerous Finding
A world-writable file that’s touched by any privileged process – a cron job, a service startup script, a sudo-executed command – is a direct path to privilege escalation. This is not an edge case. A single world-writable script in root’s cron in a root shell.
# The must-run command on every system you assess
find / -not -path "/proc/*" -perm -o+w -type f 2>/dev/null
# also check world-writable directories -- especially in PATH
find / -not -path "/proc/*" -perm -o+w -type d 2>/dev/null
# Check for world-writable directories in root's PATH
echo $PATH | tr ':' '\n' | xargs ls -ld 2>/dev/null | grep "^drwxrwxrwx"
If any world-writable file appears in the output of these commands on a production system, treat is as a critical finding until you’ve confirmed no privileged process ever touches it.
Putting It Together: A Permission Audit Workflow
Every technique above is more powerful in a sequence than in isolation. Here’s a workflow that synthesizes everything covered in our linux file permissions cheat sheet:
On initial access during a security assessment:
For ongoing hardening:
- Schedule SUID and capability audits quarterly and after every software deployment
- Include
getfacl -Rin your configuration management baseline checks - Monitor
chattrviaauditd– removal of immutable flags from auth files is a high-fidelity indicator of compromise - Keep AppArmor profiles in enforce mode and verify state after every package update
- Validate
/tmpand/tmp/varmount options after kernel updates and host migrations
The gap between a system that looks hardened and one that actually comes down to the visibility you’re willing to build. Standard tools show you most of what’s there. The techniques above show the rest.
Happy hacking!