Why Privilege Escalation Prevention Still Matters
Privilege escalation is one of those attack vectors that never really goes away. According to Elastic's 2024 Global Threat Report, brute-force attacks account for a staggering 89% of all endpoint behaviors on Linux — and many of those attacks chain local privilege escalation exploits to jump from an initial foothold to full root access. With over 5,500 kernel CVEs recorded in 2025 alone, the attack surface keeps growing. Proactive hardening isn't optional anymore; it's table stakes.
So, let's dive into the practical side. This guide walks you through auditing and reducing SUID/SGID binaries, replacing them with fine-grained Linux capabilities, locking down sudo, hardening Polkit, applying restrictive mount options, and monitoring for unauthorized changes. Every technique includes commands you can run on production servers today.
Auditing and Reducing SUID/SGID Binaries
When an executable has the SUID (Set User ID) bit set, it runs with the effective user ID of the file owner — typically root — no matter who launched it. SGID (Set Group ID) works the same way but for group permissions. Attackers know this, and scanning for these binaries is usually their first move when attempting local privilege escalation.
Discovering SUID and SGID Binaries
Start by inventorying every SUID and SGID binary on your system:
# Find all SUID binaries
find / -perm -4000 -type f 2>/dev/null
# Find all SGID binaries
find / -perm -2000 -type f 2>/dev/null
# Find both SUID and SGID at once
find / \( -perm -4000 -o -perm -2000 \) -type f -exec ls -la {} \; 2>/dev/null
Save this output as your baseline. A typical minimal server installation has somewhere between 15 and 25 SUID binaries. If you're seeing significantly more than that, it's worth investigating each one.
Deciding What to Keep
Not all SUID binaries are unnecessary — some are genuinely required for normal system operation. Here's a rough classification:
- Usually required:
sudo,su,passwd,mount,umount,newgrp - Often removable:
ping(can use capabilities instead),pkexec(if you're not using Polkit for desktop authentication),chfn,chsh, legacy network tools - Check with GTFOBins: Cross-reference any SUID binary you don't recognize against the GTFOBins database, which catalogs binaries that can be abused for privilege escalation
Removing Unnecessary SUID/SGID Bits
Once you've identified binaries that don't need elevated privileges, strip the special permission bits:
# Remove SUID bit from a specific binary
chmod u-s /usr/bin/chfn
chmod u-s /usr/bin/chsh
# Remove both SUID and SGID in one command
chmod a-s /usr/bin/some-binary
# Verify the change
ls -la /usr/bin/chfn
Important: Document every change you make. If a package update reinstates the SUID bit, your file integrity monitoring should catch it and alert you.
Replacing SUID with Linux Capabilities
Linux capabilities split the monolithic root privilege into over 40 discrete units. Instead of granting full root access through SUID, you assign only the specific capability a program actually needs. This dramatically reduces the blast radius if an attacker manages to exploit a vulnerable binary.
Honestly, capabilities are one of the most underused security features on Linux. If you're still relying heavily on SUID, this section alone could significantly improve your security posture.
Understanding Capability Sets
Every process in Linux carries three capability sets:
- Permitted (p): The maximum set of capabilities the process can use
- Effective (e): The capabilities currently active for privilege checks
- Inheritable (i): Capabilities that can be passed to child processes
Practical Examples: Migrating from SUID to Capabilities
The classic example is ping, which needs raw socket access. Instead of running as root via SUID, you can grant it only CAP_NET_RAW:
# Remove SUID from ping
chmod u-s /usr/bin/ping
# Grant only the raw socket capability
setcap cap_net_raw+ep /usr/bin/ping
# Verify the capability
getcap /usr/bin/ping
# Output: /usr/bin/ping cap_net_raw=ep
Another common scenario — a web server that needs to bind to port 80 or 443 without running as root:
# Allow binding to privileged ports without root
setcap 'cap_net_bind_service=+ep' /usr/local/bin/my-web-app
# Verify
getcap /usr/local/bin/my-web-app
Auditing All Capabilities on Your System
Make it a habit to scan for binaries with capabilities set. An attacker might quietly add capabilities to a binary they control:
# Recursively scan the entire filesystem for capability-bearing files
getcap -r / 2>/dev/null
Any capability-bearing binary you didn't explicitly configure warrants immediate investigation. No exceptions.
Dangerous Capabilities to Watch For
Some capabilities are nearly as dangerous as full root. Keep a close eye on these:
CAP_SYS_ADMIN— grants almost complete administrative privileges, including mounting filesystems and loading kernel modules. Treat this as equivalent to root.CAP_SETUID/CAP_SETGID— allows changing user or group IDs, enabling direct privilege escalationCAP_DAC_OVERRIDE— bypasses all file permission checksCAP_NET_ADMIN— allows full network configuration changes
If you find any of these on non-system binaries, treat it as a potential compromise indicator and investigate immediately.
Hardening Sudo Configuration
Sudo is the most common privilege escalation mechanism in daily Linux administration — and, unfortunately, one of the most frequently misconfigured. I've seen production environments where a single overly permissive sudoers line gave every developer full, unrestricted root access. Don't be that sysadmin.
The Golden Rules of Sudoers Configuration
# ALWAYS edit sudoers with visudo — never use vim/nano directly
sudo visudo
# Or create drop-in files (preferred for maintainability)
sudo visudo -f /etc/sudoers.d/webadmins
Restricting Commands with Exact Paths
Never grant unrestricted sudo access. Define exactly which commands each user or group can run:
# BAD — gives full root access
developer ALL=(ALL:ALL) ALL
# GOOD — restrict to specific commands with full paths
Cmnd_Alias WEB_SERVICES = /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl restart php-fpm, \
/usr/bin/systemctl status nginx
%webadmins ALL=(root) WEB_SERVICES
Preventing Shell Escapes
Here's something that catches people off guard: certain binaries allowed through sudo can spawn shells, completely bypassing your restrictions. This is called "living off the land," and the common offenders include:
vi/vim— can execute:!bashtar— can run--checkpoint-action=exec=/bin/shfind— can execute-exec /bin/sh \;less/more— can execute!bashawk— can spawn shell processes
Cross-reference your allowed sudo commands against GTFOBins. Where possible, use purpose-built wrappers or scripts instead of general-purpose utilities.
Additional Sudo Hardening Options
# /etc/sudoers.d/hardening
# Reduce credential cache timeout from default 15 min to 5 min
Defaults timestamp_timeout=5
# Require password for every sudo invocation (strictest)
# Defaults timestamp_timeout=0
# Log all sudo commands to a dedicated file
Defaults logfile="/var/log/sudo.log"
# Require a TTY for sudo (prevents some automated exploits)
Defaults requiretty
# Set a secure PATH for sudo commands
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Limit authentication attempts
Defaults passwd_tries=3
Auditing Current Sudo Permissions
# Check what the current user can run
sudo -l
# Check for users with NOPASSWD entries (high risk)
grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null
# Check for ALL permission grants
grep -r "ALL=(ALL" /etc/sudoers /etc/sudoers.d/ 2>/dev/null
Hardening Polkit (PolicyKit)
Polkit is the authorization framework that lets non-privileged processes communicate with privileged ones on modern Linux systems. It's been the target of some nasty CVEs — most notably CVE-2021-4034 (PwnKit) and CVE-2021-3560, both of which allowed unprivileged users to gain root access with trivial effort.
Immediate Polkit Hardening Steps
# Check if pkexec has SUID set
ls -la /usr/bin/pkexec
# If your system does not require pkexec, remove the SUID bit
chmod u-s /usr/bin/pkexec
# Verify Polkit version — ensure patches for CVE-2021-4034 are applied
pkexec --version
# On RHEL/CentOS, check if the patch is installed
rpm -q polkit --changelog | head -20
# On Debian/Ubuntu
apt changelog policykit-1 2>/dev/null | head -20
Restricting Polkit Rules
For servers that don't need desktop authentication dialogs, you can lock things down with a blanket deny rule:
/* /etc/polkit-1/rules.d/99-deny-all.rules
Deny all Polkit authorization requests by default on servers */
polkit.addRule(function(action, subject) {
return polkit.Result.NO;
});
Warning: Test this in a staging environment first. Some system services rely on Polkit for legitimate operations, and a blanket deny could break things in unexpected ways.
Hardening Filesystem Mount Options
Temporary directories like /tmp, /var/tmp, and /dev/shm are favorite staging areas for attackers. By applying restrictive mount options, you can prevent execution of uploaded malicious binaries and block SUID exploitation from these paths. It's a quick win that takes minutes to implement.
The Three Critical Mount Options
nosuid— ignores SUID and SGID bits on all files in this mount, preventing privilege escalation through uploaded binariesnoexec— forbids execution of any program on this mount point, stopping malware from running directlynodev— ignores device files, preventing creation of fake device nodes
Recommended /etc/fstab Configuration
# /etc/fstab — hardened temporary filesystems
# /tmp with all three restrictions
tmpfs /tmp tmpfs defaults,nodev,nosuid,noexec,size=2G 0 0
# Bind /var/tmp to /tmp so it inherits the same restrictions
/tmp /var/tmp none rw,noexec,nosuid,nodev,bind 0 0
# /dev/shm with restrictions
tmpfs /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0
Applying Changes Without Rebooting
# Remount /tmp with new options
mount -o remount,noexec,nosuid,nodev /tmp
# Remount /dev/shm
mount -o remount,noexec,nosuid,nodev /dev/shm
# Bind /var/tmp to /tmp
mount -o rw,noexec,nosuid,nodev,bind /tmp /var/tmp
# Verify the mount options are active
findmnt /tmp
findmnt /dev/shm
findmnt /var/tmp
Additional Mount Hardening
Don't stop at temp directories. Apply nosuid and nodev to other non-system partitions too:
# /home — users should not run SUID binaries from home directories
/dev/sda3 /home ext4 defaults,nodev,nosuid 0 2
# Removable media
/dev/sr0 /media/cdrom auto noauto,nodev,nosuid,noexec 0 0
One caveat: Setting noexec on /tmp can interfere with some package managers and installers that use /tmp for temporary scripts. If you run into this, configure the installer's temporary directory via environment variables (e.g., TMPDIR=/var/cache/apt/tmp) rather than relaxing the mount options. Don't compromise your security for convenience.
Kernel-Level Privilege Escalation Defenses
Beyond file permissions and mount options, the Linux kernel itself provides several sysctl parameters that directly harden against privilege escalation techniques. These are often overlooked, but they're some of the most effective defenses you can enable.
Essential sysctl Hardening Parameters
# /etc/sysctl.d/99-privilege-escalation-hardening.conf
# Restrict access to kernel pointers (prevents KASLR bypass)
kernel.kptr_restrict = 2
# Restrict access to dmesg (prevents kernel info leaks)
kernel.dmesg_restrict = 1
# Disable kernel profiling for non-root users
kernel.perf_event_paranoid = 3
# Restrict ptrace to parent processes only (prevents credential dumping)
kernel.yama.ptrace_scope = 2
# Restrict unprivileged access to BPF (prevents kernel inspection)
kernel.unprivileged_bpf_disabled = 1
# Restrict userns cloning (prevents namespace-based escapes)
kernel.unprivileged_userns_clone = 0
# Protect symlinks and hardlinks against TOCTOU attacks
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
# Protect FIFOs and regular files in world-writable sticky dirs
fs.protected_fifos = 2
fs.protected_regular = 2
Applying Kernel Parameters
# Apply immediately
sysctl --system
# Or apply a specific file
sysctl -p /etc/sysctl.d/99-privilege-escalation-hardening.conf
# Verify a specific parameter
sysctl kernel.yama.ptrace_scope
Monitoring and File Integrity
Here's the thing — hardening isn't a "set it and forget it" exercise. Attackers can modify binaries, add SUID bits, or inject capabilities after your initial hardening. Continuous monitoring is what catches these changes before they lead to a full compromise.
Setting Up AIDE for SUID/Capability Monitoring
# Install AIDE
apt install aide # Debian/Ubuntu
dnf install aide # RHEL/Fedora
# Configure AIDE to watch for permission changes on critical binaries
# Add to /etc/aide/aide.conf:
/usr/bin PERMS+CAPS
/usr/sbin PERMS+CAPS
/usr/local/bin PERMS+CAPS
/usr/local/sbin PERMS+CAPS
# Initialize the AIDE database
aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Run a check
aide --check
Auditd Rules for Privilege Escalation Detection
# /etc/audit/rules.d/privilege-escalation.rules
# Monitor changes to SUID/SGID bits
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat -F auid>=1000 -F perm=s -k suid_change
# Monitor setcap/getcap usage
-w /usr/sbin/setcap -p x -k capability_change
-w /usr/sbin/getcap -p x -k capability_audit
# Monitor sudo configuration changes
-w /etc/sudoers -p wa -k sudoers_change
-w /etc/sudoers.d/ -p wa -k sudoers_change
# Monitor Polkit rule changes
-w /etc/polkit-1/rules.d/ -p wa -k polkit_change
-w /usr/share/polkit-1/rules.d/ -p wa -k polkit_change
# Monitor privilege escalation attempts
-a always,exit -F arch=b64 -S setuid,setgid,setreuid,setregid -F auid>=1000 -k priv_escalation
Loading and Verifying Audit Rules
# Reload audit rules
augenrules --load
# Verify rules are active
auditctl -l | grep -E "suid|capability|sudoers|polkit|priv_escalation"
# Search for recent privilege escalation events
ausearch -k priv_escalation --start recent
Automated Hardening Audit Script
I put together a comprehensive audit script that checks all the hardening measures we've covered. You can run it periodically or (even better) integrate it into your CI/CD pipeline for infrastructure-as-code deployments:
#!/bin/bash
# privilege-escalation-audit.sh — Audit script for privilege escalation hardening
# Run as root: sudo bash privilege-escalation-audit.sh
set -euo pipefail
echo "===== SUID/SGID Binary Audit ====="
echo "SUID binaries found:"
find / \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | sort
echo ""
echo "===== Capability Audit ====="
echo "Binaries with capabilities:"
getcap -r / 2>/dev/null
echo ""
echo "===== Sudo Configuration Audit ====="
echo "Users/groups with NOPASSWD:"
grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null || echo "None found (good)"
echo ""
echo "Entries with ALL=(ALL):"
grep -r "ALL=(ALL" /etc/sudoers /etc/sudoers.d/ 2>/dev/null || echo "None found"
echo ""
echo "===== Mount Option Audit ====="
for mount_point in /tmp /var/tmp /dev/shm; do
opts=$(findmnt -n -o OPTIONS "$mount_point" 2>/dev/null || echo "NOT MOUNTED SEPARATELY")
echo "$mount_point: $opts"
if echo "$opts" | grep -q "nosuid" && echo "$opts" | grep -q "noexec"; then
echo " Status: HARDENED"
else
echo " Status: WARNING — missing nosuid/noexec"
fi
done
echo ""
echo "===== Kernel Hardening Parameters ====="
params=(
"kernel.kptr_restrict"
"kernel.dmesg_restrict"
"kernel.perf_event_paranoid"
"kernel.yama.ptrace_scope"
"fs.protected_symlinks"
"fs.protected_hardlinks"
"fs.protected_fifos"
"fs.protected_regular"
)
for param in "${params[@]}"; do
value=$(sysctl -n "$param" 2>/dev/null || echo "NOT SET")
echo " $param = $value"
done
echo ""
echo "===== Polkit Audit ====="
if [ -f /usr/bin/pkexec ]; then
perms=$(stat -c "%A" /usr/bin/pkexec)
echo "pkexec permissions: $perms"
if echo "$perms" | grep -q "s"; then
echo " Status: WARNING — SUID bit is set"
else
echo " Status: HARDENED — no SUID"
fi
else
echo "pkexec not found (OK for minimal server installs)"
fi
echo ""
echo "===== Audit Complete ====="
Putting It All Together: A Defense-in-Depth Checklist
Privilege escalation prevention isn't about a single fix. It's about layering multiple controls so that bypassing one doesn't hand an attacker full access. Here's the complete hardening workflow:
- Audit SUID/SGID binaries and remove the unnecessary ones. Document your baseline.
- Migrate from SUID to capabilities where possible (ping, custom network services, etc.).
- Lock down sudo with exact command paths, group-based permissions, and short credential timeouts.
- Harden Polkit by patching, removing pkexec SUID, and restricting authorization rules on servers.
- Apply restrictive mount options (nosuid, noexec, nodev) to /tmp, /var/tmp, /dev/shm, and /home.
- Set kernel hardening parameters via sysctl to restrict ptrace, BPF, kernel pointers, and link attacks.
- Deploy file integrity monitoring (AIDE) and audit rules (auditd) to detect unauthorized changes.
- Combine with MAC frameworks — SELinux or AppArmor provide an additional layer that limits damage even if privilege escalation succeeds.
- Run the audit script regularly and integrate it into your configuration management pipeline.
Frequently Asked Questions
What is the difference between SUID and Linux capabilities?
SUID grants a binary the full privileges of its owner (typically root), while Linux capabilities break root privileges into over 40 discrete units. Capabilities follow the principle of least privilege — you grant a program only the specific privilege it needs (like binding to a low port) without giving it full root access. The difference in attack surface is significant.
How do I find all SUID binaries on my Linux system?
Run find / -perm -4000 -type f 2>/dev/null to list all SUID files. For SGID files, use find / -perm -2000 -type f 2>/dev/null. Save this output as a baseline, cross-reference against GTFOBins for known exploitation techniques, and remove the SUID bit from any binary that doesn't need it using chmod u-s /path/to/binary.
Can setting noexec on /tmp break software installations?
It can, yes. Some package managers and installers (notably certain apt configurations) use /tmp for temporary scripts during installation. The fix isn't to remove noexec though — configure the installer to use an alternative temporary directory via the TMPDIR environment variable instead. For apt specifically, you can set Dir::Exec::ForkMethod "SysCall" or configure a different temp directory in /etc/apt/apt.conf.d/.
How often should I audit privilege escalation vectors on production servers?
Run automated audits at least weekly, with real-time monitoring through AIDE and auditd for immediate detection. After any system update or package installation, re-run the SUID/capability audit — package updates can reinstate SUID bits you previously removed. For compliance frameworks like CIS Benchmarks, quarterly manual reviews are recommended alongside continuous automated scanning.
Is removing pkexec SUID safe on a server without a desktop environment?
Generally, yes. On headless servers without a desktop environment, removing the SUID bit from pkexec is safe and recommended. Polkit is primarily used for desktop authentication dialogs. That said, some system services do use Polkit for authorization, so test in staging first and verify that no installed services depend on it before making the change in production.