SUID vs. Linux Capabilities: Two Very Different Approaches to Privilege
If you’ve been managing Linux servers for a while, you’ve probably run into SUID binaries — those files with the sticky s bit that let a process run with the file owner’s privileges instead of the caller’s. Classic examples: ping, passwd, sudo. They work, but they come with a problem that gets harder to ignore the more you think about it.
SUID is all-or-nothing. When ping runs as root, it gets all root privileges — filesystem access, process control, network configuration, the works — even though all it actually needs is the ability to open a raw socket. That’s a huge attack surface for what’s essentially a diagnostic tool.
Capabilities exist to solve this exact problem. Instead of granting full root privileges, you break those privileges into discrete units and assign only what a process genuinely needs. ping needs CAP_NET_RAW. Nothing else. No SUID required.
Modern Ubuntu and Debian distributions already ship ping this way. Run this and you’ll see:
getcap /usr/bin/ping
# Output: /usr/bin/ping cap_net_raw=ep
No SUID bit. Just the one capability it needs. That’s the whole idea.
Pros and Cons: Capabilities vs. SUID
SUID — the familiar devil
Pros:
- Dead simple to set:
chmod u+s /path/to/binary - Universally supported across all Linux versions
- No kernel module or feature flag needed
- Works transparently — the binary doesn’t need to know about it
Cons:
- Binary gets full UID 0 privileges — far more than it needs
- Any vulnerability in that binary becomes a full root escalation
- Hard to audit:
find / -perm -4000gives you a list but no context on why - Applies to any user who can execute the file — no per-user control
Linux Capabilities — the surgical approach
Pros:
- Principle of least privilege: grant only what’s needed
- If the binary is exploited, the attacker gets those specific capabilities — not full root
- Auditable with
getcap -r /— shows exactly what capabilities exist and where - Per-binary granularity, supports thread-level capability management in code
- Compatible with security frameworks like AppArmor and SELinux
Cons:
- Not all software understands or requests capabilities — legacy tools may still need SUID
- Capability flags (
e,p,i) have a learning curve - Capabilities on files are stored in extended attributes — some filesystems or backup tools strip them
- Scripting languages (Python, Perl) don’t inherit file capabilities automatically — needs a wrapper binary
Recommended Setup: When to Use What
Here’s how I think about this on production systems:
- Use capabilities for compiled binaries that need one specific privilege: network tools (
tcpdump,ping), time sync daemons, packet capture agents - Keep SUID for system tools where the full privilege model is intentional and well-audited:
sudo,su,passwd— these are designed around root access - Avoid SUID entirely for anything you compiled yourself or third-party tools you’re deploying — use capabilities or run as a dedicated low-privilege user with only the capabilities it needs
- Run services as dedicated users combined with capabilities — this is the cleanest model for daemons
Practical example from my own Ubuntu 22.04 server: I had a custom packet analyzer running as root via a SUID wrapper. Moving it to a dedicated netmon system user with just CAP_NET_RAW and CAP_NET_ADMIN on the binary took about ten minutes. Same behavior. Attack surface shrunk considerably. That’s the kind of quick win that makes hardening feel less like a slog.
Implementation Guide
Install the tools
On Debian/Ubuntu:
sudo apt install libcap2-bin
On RHEL/CentOS/AlmaLinux:
sudo dnf install libcap
This gives you getcap, setcap, and capsh.
Understanding capability flags
When you see output like cap_net_raw=ep, the letters after = are the capability sets:
e— Effective: capability is active right nowp— Permitted: capability can be activatedi— Inheritable: passed to child processes
For most use cases, =ep is what you want — the capability is both permitted and effective when the binary runs.
Check existing capabilities
# Check a specific file
getcap /usr/bin/ping
# Recursively audit your entire filesystem (run as root)
getcap -r / 2>/dev/null
That recursive scan is something I run quarterly on production servers. It’s a quick sanity check — if something shows up that you didn’t put there, that’s worth investigating.
Set capabilities on a binary
Let’s say you have a custom tool /usr/local/bin/netmon that needs raw socket access:
# Grant CAP_NET_RAW (raw socket access)
sudo setcap cap_net_raw=ep /usr/local/bin/netmon
# Verify
getcap /usr/local/bin/netmon
# Output: /usr/local/bin/netmon cap_net_raw=ep
For a packet capture tool that also needs to set interface modes:
sudo setcap cap_net_raw,cap_net_admin=ep /usr/local/bin/netmon
Remove capabilities
# Remove all capabilities from a binary
sudo setcap -r /usr/local/bin/netmon
# Verify it's clean
getcap /usr/local/bin/netmon
# (no output = no capabilities)
Replace an existing SUID binary with capabilities
Concrete example: stripping SUID from tcpdump and switching to capabilities instead.
# Check current state
ls -la /usr/bin/tcpdump
# -rwxr-sr-x or -rwsr-xr-x depending on distro
# Remove SUID bit
sudo chmod u-s /usr/bin/tcpdump
# Grant only what it needs
sudo setcap cap_net_raw,cap_net_admin=ep /usr/bin/tcpdump
# Test as a regular user
tcpdump -i eth0 -c 5
Capabilities for services running as non-root users
The pattern I reach for most often: create a dedicated user, set capabilities on the binary, done.
# Create a system user (no login shell, no home dir)
sudo useradd -r -s /usr/sbin/nologin netmon
# Set capabilities on the binary
sudo setcap cap_net_raw=ep /usr/local/bin/netmon
# Run the service as that user (in systemd unit)
# [Service]
# User=netmon
# ExecStart=/usr/local/bin/netmon
# Verify the process has the right capabilities at runtime
cat /proc/$(pgrep netmon)/status | grep Cap
The /proc/PID/status output shows capability bitmasks. To decode them:
capsh --decode=0000000000002000
# 0x0000000000002000=cap_net_raw
Common capabilities reference
cap_net_raw— Raw and packet sockets (ping, tcpdump)cap_net_admin— Network interface config, routing, iptablescap_net_bind_service— Bind to ports below 1024 without rootcap_sys_time— Set system clock (ntpd, chrony)cap_dac_override— Bypass file read/write permission checkscap_setuid/cap_setgid— Change process UID/GIDcap_sys_ptrace— Trace arbitrary processes (debuggers, strace)
Full list: man 7 capabilities
One gotcha: capabilities and scripts
Setting capabilities on a Python or Bash script won’t work — the kernel only applies file capabilities to ELF binaries, not interpreted scripts. Two options: write a small C wrapper, or use ambient capabilities via systemd’s AmbientCapabilities= directive in the unit file:
[Service]
User=netmon
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW
ExecStart=/usr/bin/python3 /opt/netmon/monitor.py
Cleanest approach for Python services that need a single capability — no SUID, no running as root.
Quick audit script
Drop this in your toolkit — it shows both SUID binaries and capability-enabled files side by side:
#!/bin/bash
echo "=== SUID Binaries ==="
find / -perm -4000 -type f 2>/dev/null | sort
echo ""
echo "=== Files with Capabilities ==="
getcap -r / 2>/dev/null | sort
Run it before and after any system change for a clear picture of your privilege surface. On a well-configured system, the SUID list should be short and predictable — and every entry in the capabilities list should have a documented reason for being there.
Capabilities aren’t magic. You still need to think carefully about what each binary actually needs — get it wrong and you’ve just replaced one overprivileged binary with another. Used correctly, though, they’re a genuine upgrade over SUID, and hardening audits start feeling a lot less like archaeology.

