Skip to main content
RunBook Academy

LinuxXXX · Linux Capabilities and PrivilegeFile capabilities

File capabilities - replacing setuid with fine-grained privileges

Intermediate⏱ ~12 minsetcapgetcap

What you'll learn

  • Grant a file capability to a binary
  • Replace a setuid binary with a file-capability binary
  • Troubleshoot missing capabilities
  • Recognise when file capabilities are not enough
  • Refuse capability grants on interpreters and shells
  • Audit capability grants separately from setuid binaries
  • Use systemd AmbientCapabilities= instead of setcap on packaged binaries

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

File capabilities are stored in the file’s extended attributes (xattrs). When the file is exec’d, the kernel adds the capabilities to the process. This is the modern replacement for setuid.

Grant a capability

Grant the capability to a single-purpose service binary - one that only ever does its own job.

sudo setcap cap_net_bind_service=+ep /usr/local/bin/myservice

The syntax: cap_<NAME>=<flags>. The flags:

  • e: effective
  • p: permitted
  • i: inheritable

+ep means “add to effective and permitted”.

Verify

getcap /usr/local/bin/myservice
# /usr/local/bin/myservice cap_net_bind_service=ep

The ep means the capability is effective and permitted when the binary runs.

Never setcap an interpreter or a shell

The capability lives on the inode and is applied at execve(). The kernel does not know or care what the binary is about to run. So a capability on an interpreter is a capability on every script anyone runs through it.

# NEVER do any of these:
#   sudo setcap cap_net_bind_service=+ep /usr/bin/python3.11
#   sudo setcap cap_dac_override=+ep /bin/bash
#   sudo setcap cap_net_raw=+ep /usr/bin/perl

If /usr/bin/python3.11 carries cap_net_bind_service, then any local user can run python3.11 -c '<their own code>' and that code holds the capability. With cap_net_bind_service they can squat on port 22, 25 or 443 and impersonate a service. Apply the same pattern with cap_dac_read_search or cap_sys_admin

  • both of which appear elsewhere in this course on purpose-built binaries - and you have handed every local user root equivalence in one command.

The same rule covers bash, perl, ruby, node, awk, find (it has -exec) and anything else that executes input it was given.

Run a service with file capabilities

A web server that needs to bind to port 80. The obvious recipe does not work:

# WRONG - looks right, fails at start-up
sudo setcap cap_net_bind_service=+ep /usr/sbin/nginx
sudo -u nginx /usr/sbin/nginx
# nginx: [emerg] open() "/run/nginx.pid" failed (13: Permission denied)

The capability is granted correctly. Binding port 80 is no longer the problem. Everything else is: the master process still has to write /run/nginx.pid, /var/log/nginx and /var/lib/nginx, and the user directive in nginx.conf needs real root to setuid() the workers. A capability solves exactly one privilege check, and a daemon usually needs more than one.

Grant the capability to the service instance instead:

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
User=nginx
Group=nginx
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
RuntimeDirectory=nginx
LogsDirectory=nginx

AmbientCapabilities= gives the capability to this service instance only - not to every user who can execute the binary. CapabilityBoundingSet= caps what the process can ever hold. RuntimeDirectory= and LogsDirectory= give the unprivileged master the writable /run/nginx and /var/log/nginx it needs.

If the only requirement is a low port, skip capabilities altogether:

# Let unprivileged processes bind from port 80 upwards
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
# Persist in /etc/sysctl.d/99-unprivileged-ports.conf

Remove a capability

sudo setcap -r /usr/local/bin/myservice

The -r removes all capabilities from the file.

Replace setuid

A setuid binary like ping:

-rwsr-xr-x 1 root root 72K Apr 12  /bin/ping

The s means setuid - the binary runs as root. A compromised ping process has all of root.

With file capabilities:

sudo setcap cap_net_raw+ep /usr/bin/ping

Now ping runs as the invoking user (not root) but holds CAP_NET_RAW - permission to open a raw socket, which is the one thing it could not do unprivileged.

CAP_NET_RAW is the whole requirement. It is tempting to add CAP_NET_ADMIN because the name sounds like “more networking”, and you will find that pairing in old blog posts. Do not copy it. CAP_NET_ADMIN permits interface configuration, routing-table changes, netfilter rule manipulation and putting an interface into promiscuous mode. Attached to /usr/bin/ping it means any memory-safety or argument-handling bug in a world-executable binary escalates into rewriting the host’s routes and firewall.

Check what your distribution itself grants before inventing a value:

Read-only / Safewhat the distribution actually grants
$ getcap -r /usr/bin 2>/dev/null
/usr/bin/ping cap_net_raw=ep
/usr/bin/mtr-packet cap_net_raw=ep

cap_net_raw only. Upstream iputils dropped its need for CAP_NET_ADMIN years ago. Grant the narrowest capability that makes the tool work, then re-test - if it still works, you were right.

Common patterns

# ping and traceroute: cap_net_raw only
sudo setcap cap_net_raw+ep /usr/bin/ping
sudo setcap cap_net_raw+ep /usr/bin/traceroute

# tcpdump: cap_net_raw only
# (a packet socket sets promiscuous mode via
#  PACKET_ADD_MEMBERSHIP, which needs CAP_NET_RAW, not
#  CAP_NET_ADMIN)
sudo setcap cap_net_raw+ep /usr/bin/tcpdump

# Web servers: prefer a systemd unit with
# AmbientCapabilities=CAP_NET_BIND_SERVICE over setcap on
# the packaged binary - see the nginx section above.

# Cron: cap_dac_override (read /etc/cron.allow)
# (Rarely needed; cron runs as root by default)

File systems and capabilities

File capabilities require:

  • The filesystem must support xattrs (ext4, xfs, btrfs do; some others do not).
  • The capability xattr namespace: security.capability.
  • The binary must NOT be on a filesystem mounted with nosuid.
# Check filesystem
mount | grep 'on / '
# /dev/sda1 on / type ext4

# Check capability
getcap /usr/sbin/nginx

If a binary does not have the expected capability:

  • The filesystem does not support xattrs.
  • The mount includes nosuid.
  • The capability was not actually set.

Audit capabilities on the fleet

File capabilities and the setuid bit are two independent mechanisms. A capability-bearing binary usually has no setuid bit at all - removing the setuid bit is the entire point of using capabilities. So a setuid scan finds zero capability grants. This is the single most common mistake in a capability audit:

# WRONG - this is a setuid audit wearing a capability label.
# getcap is only called on files that already matched -perm -u+s,
# and /usr/bin/ping (cap_net_raw=ep, mode 0755) is not one of them.
for f in $(find / -xdev -type f -perm -u+s); do
    echo "$f $(getcap $f 2>/dev/null)"
done

The unquoted $f also breaks on any path containing a space, and -xdev silently skips every separately-mounted filesystem - including /usr and /opt on many builds.

Run two separate audits:

# 1. Capability-bearing binaries, across every local filesystem
for mp in $(findmnt -rno TARGET -t ext4,xfs,btrfs); do
    sudo getcap -r "$mp" 2>/dev/null
done | sort > /tmp/caps.now

# 2. setuid and setgid binaries - a different question
sudo find / -xdev -type f \( -perm -4000 -o -perm -2000 \) \
    -printf '%M %p\n' 2>/dev/null | sort > /tmp/suid.now

# 3. Drift against the approved baselines
diff -u /etc/baseline/caps.approved /tmp/caps.now
diff -u /etc/baseline/suid.approved /tmp/suid.now

getcap -r reads the security.capability xattr directly, so it finds a grant regardless of file mode. Iterating the mount points instead of relying on -xdev means a binary hidden on a separate /opt mount still shows up.

A non-empty diff on caps.now is an incident until proven otherwise: someone attached a capability to a binary outside configuration management.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A colleague runs setcap cap_net_bind_service=+ep /usr/bin/python3.11 so their app can bind port 80. What is the consequence on a shared host?

  2. Q2. File capabilities require the filesystem to support xattrs and not be mounted with nosuid.

  3. Q3. Which of the following commands grant file capabilities? Select all that apply.

  4. Q4. Your capability audit prints nothing, yet `ls -l /usr/bin/ping` shows -rwxr-xr-x and `getcap /usr/bin/ping` shows cap_net_raw=ep. The audit loops over `find / -xdev -type f -perm -u+s` and calls getcap on each hit. What is wrong?

  5. Q5. A change request asks you to set cap_net_raw,cap_net_admin+ep on /usr/bin/traceroute so it works for unprivileged users. What should you do?

Passing score: 75%. Answers are checked in this browser.