LinuxXXX · Linux Capabilities and PrivilegeFile capabilities
File capabilities - replacing setuid with fine-grained privileges
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
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: effectivep: permittedi: 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:
$ getcap -r /usr/bin 2>/dev/null/usr/bin/ping cap_net_raw=ep
/usr/bin/mtr-packet cap_net_raw=epcap_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
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?
Q2. File capabilities require the filesystem to support xattrs and not be mounted with nosuid.
Q3. Which of the following commands grant file capabilities? Select all that apply.
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?
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.