Mastering Linux Capabilities: Fine-Grained Process Permissions with getcap and setcap as a SUID Alternative

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

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 -4000 gives 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 now
  • p — Permitted: capability can be activated
  • i — 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, iptables
  • cap_net_bind_service — Bind to ports below 1024 without root
  • cap_sys_time — Set system clock (ntpd, chrony)
  • cap_dac_override — Bypass file read/write permission checks
  • cap_setuid / cap_setgid — Change process UID/GID
  • cap_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.

Share: