A port below 1024 without root
A service gets the part through its unit; a program outside systemd gets it through its file:
sysctl net.ipv4.ip_unprivileged_port_start=80 opens the port too, but
for every user of the machine at once.
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.
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
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 |
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 |
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:
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.
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:
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:
The part goes to a copy of Python: setcap does not work on the symbolic
link /usr/bin/python3.
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.
capsh --drop runs a command as root with the named parts removed from
the bounding set:
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.
systemd gives and takes parts for a service. AmbientCapabilities= gives
a service with User= the part it needs; CapabilityBoundingSet= sets the
ceiling:
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).
A service gets the part through its unit; a program outside systemd gets it through its file:
sysctl net.ipv4.ip_unprivileged_port_start=80 opens the port too, but
for every user of the machine at once.
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.
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.
setcap ran without sudo: only root can give parts to files.
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.
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.
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.
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.
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.
User=, CapabilityBoundingSet= and
AmbientCapabilities= in a unit./proc/<PID>/status and other process files.cap_net_raw.Found an inaccuracy? Report an error in the article