Where to start First steps track · 15 stages
Linux Labs

Capabilities: root split into parts

12 min Ubuntu 24.04 · Linux 6.8 · systemd 255

Parts of root's rights: a process or a file gets only the one it needs, e.g. CAP_NET_BIND_SERVICE for a low port.

Cheat sheet

7 commands Printable version
  • grep Cap /proc/self/status sets of the current process
  • capsh --decode=0000000000000400 mask → names
  • getpcaps 1234 capabilities of a process by PID
  • getcap -r / 2>/dev/null files with capabilities
  • sudo setcap cap_net_bind_service=+ep /usr/local/bin/app give a file
  • sudo setcap -r /usr/local/bin/app remove
  • sudo capsh --drop=cap_dac_override -- -c 'cat f' root minus a part

Overview

The kernel checks rights in two ways. An ordinary process is checked against the file's permissions and owner. A root process used to be allowed everything. Now root's power is split into parts — capabilities — and the kernel asks for a specific part: listening on a port below 1024 takes CAP_NET_BIND_SERVICE, reading someone else's file takes CAP_DAC_OVERRIDE.

Root has every part by default, which is why it can do anything. But a root process without the needed part is refused, and an ordinary user's process with one part can do what that part allows and nothing more. This is the basis of least privilege: a service gets its part, not all of root.

The machine has 41 of them. capsh --decode=000001ffffffffff prints the list; the common ones:

Capability What it allows Who needs it
CAP_NET_BIND_SERVICE listen on a port below 1024 a web server on 80 and 443
CAP_NET_RAW raw sockets: ICMP, crafted packets ping, mtr
CAP_NET_ADMIN interfaces, routes, firewall rules systemd-networkd, ip, nft
CAP_DAC_OVERRIDE bypass rwx file permissions root reading someone else's file
CAP_DAC_READ_SEARCH read any file and enter any directory backups
CAP_CHOWN change a file's owner package installation
CAP_SETUID, CAP_SETGID change the process UID and GID su, sudo, sshd
CAP_KILL signal other users' processes kill as root
CAP_SYS_TIME set the system clock systemd-timesyncd
CAP_SYS_PTRACE trace other processes strace -p
CAP_SYS_ADMIN mounting and dozens of other operations almost all of root

Process sets

A process has five sets. The kernel keeps them as bit masks and shows them in /proc/<PID>/status:

Line Set Meaning
CapEff effective in force now — the kernel checks this one
CapPrm permitted what the process may turn on in effective
CapInh inheritable what may pass to a new program through exec
CapBnd bounding the ceiling: neither the process nor its children get more
CapAmb ambient passes to a new program even without file capabilities
text
student@ubuntu:~$ grep Cap /proc/self/status
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	000001ffffffffff
CapAmb:	0000000000000000
student@ubuntu:~$ sudo grep Cap /proc/self/status
CapInh:	0000000000000000
CapPrm:	000001ffffffffff
CapEff:	000001ffffffffff
CapBnd:	000001ffffffffff
CapAmb:	0000000000000000

student has no effective parts; a process under sudo has all 41. capsh --decode=<mask> turns a mask into names, and getpcaps shows a process by PID:

text
student@ubuntu:~$ getpcaps $(pgrep -xo systemd-timesyn)
456: cap_sys_time=eip
student@ubuntu:~$ getpcaps $(pgrep -xo systemd-resolve)
451: cap_net_raw=eip
student@ubuntu:~$ getpcaps $(pgrep -xo nginx)
611: =ep

The letters after = are sets: e effective, i inheritable, p permitted. The time and name services run as their own users with one part each. =ep with no names means every part: the nginx main process runs as root.

File capabilities

An executable file can carry capabilities too — like setuid, except that the process gets the named parts instead of all of root. The machine has three such files:

text
student@ubuntu:~$ getcap -r / 2>/dev/null
/usr/bin/mtr-packet cap_net_raw=ep
/usr/bin/ping cap_net_raw=ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper
  cap_net_bind_service,cap_net_admin,cap_sys_nice=ep

That is why ping works for student: unprivileged ICMP is turned off on the machine (net.ipv4.ping_group_range = 1 0), and the program gets CAP_NET_RAW from its file.

setcap gives parts to a file, and only root can run it. The test is a script, bind81.py, that takes port 81:

python
import socket
s = socket.socket()
s.bind(('127.0.0.1', 81))
print('bound 81')

The part goes to a copy of Python: setcap does not work on the symbolic link /usr/bin/python3.

bash
sudo cp /usr/bin/python3.12 /usr/local/bin/py-bind
sudo setcap cap_net_bind_service=+ep /usr/local/bin/py-bind
text
student@ubuntu:~$ python3 bind81.py
PermissionError: [Errno 13] Permission denied
student@ubuntu:~$ getcap /usr/local/bin/py-bind
/usr/local/bin/py-bind cap_net_bind_service=ep
student@ubuntu:~$ /usr/local/bin/py-bind bind81.py
bound 81

A process started from that file is an ordinary student with one part: its CapEff is 0000000000000400, which is cap_net_bind_service. The unprivileged port boundary is net.ipv4.ip_unprivileged_port_start = 1024.

Capabilities live in an extended attribute of the file. A plain cp does not carry them; cp -a does. A new file in place of the old one — after a package upgrade or a rebuild — comes without them: ping gets its part from the package's install script. On a file system mounted with nosuid, file capabilities have no effect.

Root without the needed part

capsh --drop runs a command as root with the named parts removed from the bounding set:

text
student@ubuntu:~$ echo secret > ~/secret; chmod 600 ~/secret
student@ubuntu:~$ sudo cat /home/student/secret
secret
student@ubuntu:~$ sudo capsh \
> --drop=cap_dac_override,cap_dac_read_search \
> -- -c 'cat /home/student/secret'
cat: /home/student/secret: Permission denied

Root is still root (UID 0), but without CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH the file's permissions apply to it as to anyone. In the same way, root without CAP_NET_BIND_SERVICE gets Permission denied on port 84.

Services and containers

systemd gives and takes parts for a service. AmbientCapabilities= gives a service with User= the part it needs; CapabilityBoundingSet= sets the ceiling:

text
student@ubuntu:~$ sudo systemd-run --pipe --wait -q -p User=demo \
> -p AmbientCapabilities=CAP_NET_BIND_SERVICE grep Cap /proc/self/status
CapInh:	0000000000000400
CapPrm:	0000000000000400
CapEff:	0000000000000400
CapBnd:	000001ffffffffff
CapAmb:	0000000000000400
student@ubuntu:~$ sudo systemd-run --pipe --wait -q \
> -p CapabilityBoundingSet=CAP_NET_BIND_SERVICE cat /home/student/secret
/usr/bin/cat: /home/student/secret: Permission denied

In the second case the service runs as root, but its ceiling is CapBnd: 0000000000000400, and it cannot read someone else's file. How to put these settings into a unit — systemd-hardening. Docker starts a container with a reduced set of parts; --cap-drop and --cap-add change it (see docker).

Recipes

3

A port below 1024 without root

A service gets the part through its unit; a program outside systemd gets it through its file:

bash
# service: drop-in /etc/systemd/system/app.service.d/port.conf
[Service]
User=app
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# program outside systemd
sudo setcap cap_net_bind_service=+ep /usr/local/bin/app

sysctl net.ipv4.ip_unprivileged_port_start=80 opens the port too, but for every user of the machine at once.

Find files with capabilities

bash
getcap -r / 2>/dev/null

A clean machine shows the three files from the block above. An extra file in the list — especially an interpreter, an archiver or an editor — is a reason to find out who gave it the part and why.

Which parts a service has

bash
getpcaps $(systemctl show -p MainPID --value nginx)
grep Cap /proc/$(systemctl show -p MainPID --value nginx)/status

Troubleshooting

5

Permission denied on a port below 1024

PermissionError: [Errno 13] Permission denied from Python, bind() to 0.0.0.0:80 failed (13: Permission denied) from nginx: the process does not run as root and lacks CAP_NET_BIND_SERVICE. Add AmbientCapabilities= to the service and check CapEff in /proc/<PID>/status.

AmbientCapabilities= is already there and the refusal stays — the part is cut off by the ceiling. An empty CapabilityBoundingSet= in another drop-in clears the ambient set too: put the same part into CapabilityBoundingSet= as well. A busy port gives a different error, Address already in use; on the lab machine nginx holds port 80.

unable to set CAP_SETFCAP effective capability

text
student@ubuntu:~$ setcap cap_net_bind_service=+ep /usr/local/bin/py-bind
unable to set CAP_SETFCAP effective capability: Operation not permitted

setcap ran without sudo: only root can give parts to files.

fatal error: Invalid argument

setcap did not understand the part name or the file. Names are as in capsh --decode (cap_net_bind_service), and the file must be a regular one: setcap does not work on a symbolic link such as /usr/bin/python3; use python3.12 itself. A missing file gives Failed to set capabilities on file '…': No such file or directory.

The part is gone after a copy or an upgrade

getcap prints nothing and the program gets Permission denied again. The file was replaced: cp without -a, a package upgrade, a rebuild. Give the part again and pin it where the file is produced — in the install script or the unit.

Root gets Operation not permitted

The process runs as UID 0 and the kernel refuses. Its ceiling is cut: a service with CapabilityBoundingSet=, a container, capsh --drop. Compare CapBnd and CapEff in /proc/<PID>/status with the part you need.

Danger zone

2

A part on an interpreter

py-bind from the example above gives a low port to any script of any user. cap_dac_read_search on Python, tar or vim opens /etc/shadow to them, and cap_setuid is a way to root. Give parts to the file of a specific program and check the getcap -r / list.

CAP_SYS_ADMIN

This part allows mounting file systems, creating namespaces and dozens of other operations. A service with it is almost root — look for a narrower part.

See also

  • systemd-hardening — User=, CapabilityBoundingSet= and AmbientCapabilities= in a unit.
  • special-bits — setuid: all of root through a file.
  • proc-filesystem — /proc/<PID>/status and other process files.
  • ping — a program with cap_net_raw.
  • docker — container capabilities.
  • apparmor — a profile limits capabilities too.
  • sudo — a whole command as root.
Loading…