Skip to content

Capabilities

Linux capabilities split root's power into about 40 separate privileges, such as binding ports below 1024 (CAP_NET_BIND_SERVICE) or changing file owners (CAP_CHOWN). Programs and services get only the ones they need, which replaces many setuid binaries and root-run daemons.

Track: Advanced · Interview weight: Med


Must-Know Facts

Fact Value Verify with
Process sets Effective (checked now), Permitted (may enable), Inheritable, Bounding (upper limit), Ambient (kept across execve for non-root) grep Cap /proc/self/status
Root A root process has all capabilities in its effective set; a normal user has none sudo grep CapEff /proc/self/status
Decode capsh --decode=HEX turns a mask into names capsh --decode=0000000000000400
File capabilities Stored in the security.capability extended attribute; setcap cap_x=+ep FILE getcap FILE
Copies cp without --preserve=xattr drops file capabilities; so does rewriting the file getcap on the copy
Low ports Ports below net.ipv4.ip_unprivileged_port_start (1024) need CAP_NET_BIND_SERVICE sysctl net.ipv4.ip_unprivileged_port_start
ping Ubuntu: file capability cap_net_raw; RHEL: unprivileged ICMP sockets via net.ipv4.ping_group_range getcap /usr/bin/ping
systemd AmbientCapabilities= grants, CapabilityBoundingSet= limits; User= alone drops everything systemctl show -p CapabilityBoundingSet UNIT
Dropping capsh --drop=cap_x -- -c CMD runs a command without that capability, even as root sudo capsh --print
Containers Docker keeps 14 capabilities by default; --cap-drop ALL --cap-add NET_BIND_SERVICE is the least-privilege pattern docker inspect
Dangerous ones CAP_SYS_ADMIN (mount, many admin calls), CAP_SYS_PTRACE, CAP_DAC_OVERRIDE, CAP_SETUID are almost root man 7 capabilities

Reading Capability Sets

A shell run by laborant on client (Ubuntu 24.04) has no effective capabilities; the same command under sudo has all of them. The bounding set limits what either can ever gain.

grep Cap /proc/self/status
sudo grep Cap /proc/self/status
capsh --decode=000001ffffffffff | tr ',' '\n' | head -5
capsh --decode=000001ffffffffff | tr ',' '\n' | wc -l

Output:

CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
0x000001ffffffffff=cap_chown
cap_dac_override
cap_dac_read_search
cap_fowner
cap_fsetid
41

Kernel 6.1 knows 41 capabilities, one bit each. Root is powerful because its effective set is full, and removing one bit removes that power even for root:

sudo touch /tmp/owned-by-root
sudo capsh --drop=cap_chown -- -c 'chown laborant /tmp/owned-by-root'; echo "exit=$?"
sudo capsh --drop=cap_chown -- -c 'grep CapEff /proc/self/status'

Output:

chown: changing ownership of '/tmp/owned-by-root': Operation not permitted
exit=1
CapEff: 000001fffffffffe

Bit 0 (cap_chown) is cleared in the last line.


How ping Works Without setuid

getcap /usr/bin/ping; echo "getcap exit=$?"
sysctl net.ipv4.ping_group_range

Output:

getcap exit=0
net.ipv4.ping_group_range = 0   2147483647

ping has no capability. The sysctl lets every group (0 to 2147483647) open unprivileged ICMP sockets.

getcap /usr/bin/ping
ls -l /usr/bin/ping
sysctl net.ipv4.ping_group_range

Output:

/usr/bin/ping cap_net_raw=ep
-rwxr-xr-x 1 root root 89800 Jul 24  2025 /usr/bin/ping
net.ipv4.ping_group_range = 1   0

The empty group range (1 to 0) disables ICMP sockets, so ping gets cap_net_raw from the file instead of a setuid bit.


File Capabilities for One Binary

An unprivileged user cannot listen on port 99. A copy of nc with cap_net_bind_service can, without any other root power:

nc -l 99; echo "exit=$?"
sudo cp /usr/bin/nc.openbsd /usr/local/bin/nc-lowport
sudo setcap cap_net_bind_service=+ep /usr/local/bin/nc-lowport
getcap /usr/local/bin/nc-lowport
timeout 2 /usr/local/bin/nc-lowport -l 99 & sleep 0.5; ss -tlnp 'sport = :99'; wait
cp /usr/local/bin/nc-lowport /tmp/nc-copy; getcap /tmp/nc-copy; echo "getcap on the copy: exit=$?"
sysctl net.ipv4.ip_unprivileged_port_start

Output:

nc: Permission denied
exit=1
/usr/local/bin/nc-lowport cap_net_bind_service=ep
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      1            0.0.0.0:99        0.0.0.0:*          
getcap on the copy: exit=0
net.ipv4.ip_unprivileged_port_start = 1024

+ep sets the capability as permitted and effective when the file runs. The copy printed nothing for getcap: the capability lived in an extended attribute that cp did not copy.

A file capability is a privilege on everything that binary can do

Giving cap_net_bind_service to a general tool such as nc or python3 lets every user of that file bind low ports. Grant capabilities to the service instead (next section), or to a dedicated binary that only root can replace.


Capabilities for a systemd Service

On web (Rocky Linux 10.2), a unit runs ncat on port 99 as the user deploy. User= starts the process with no capabilities, so the bind fails:

sudo tee /etc/systemd/system/lab-lowport.service >/dev/null <<'EOF'
[Unit]
Description=Lab listener on port 99 as an unprivileged user

[Service]
User=deploy
ExecStart=/usr/bin/ncat -lk 99
EOF
sudo systemctl daemon-reload
sudo systemctl start lab-lowport; sleep 1
systemctl is-active lab-lowport
sudo journalctl -u lab-lowport -o cat | head -4

Output:

failed
Started lab-lowport.service - Lab listener on port 99 as an unprivileged user.
Ncat: bind to :::99: Permission denied. QUITTING.
lab-lowport.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
lab-lowport.service: Failed with result 'exit-code'.

A drop-in grants the one capability through the ambient set and removes everything else from the bounding set:

sudo mkdir -p /etc/systemd/system/lab-lowport.service.d
printf '[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\n' | sudo tee /etc/systemd/system/lab-lowport.service.d/caps.conf >/dev/null
sudo systemctl daemon-reload
sudo systemctl restart lab-lowport; sleep 1
systemctl is-active lab-lowport
PID=$(systemctl show -p MainPID --value lab-lowport)
grep -E 'CapEff|CapBnd|CapAmb' /proc/$PID/status
capsh --decode=0000000000000400
sudo ss -tlnp 'sport = :99'

Output:

active
CapEff: 0000000000000400
CapBnd: 0000000000000400
CapAmb: 0000000000000400
0x0000000000000400=cap_net_bind_service
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess                         
LISTEN 0      10           0.0.0.0:99        0.0.0.0:*    users:(("ncat",pid=93287,fd=4))
LISTEN 0      10              [::]:99           [::]:*    users:(("ncat",pid=93287,fd=3))

Daemons that start as root often drop to a user and keep what they need. chronyd on the same host runs as chrony with two capabilities:

grep -E 'CapEff' /proc/$(pgrep -x chronyd)/status
capsh --decode=$(awk '/CapEff/ {print $2}' /proc/$(pgrep -x chronyd)/status)
ps -o user,pid,comm -C chronyd

Output:

CapEff: 0000000002000400
0x0000000002000400=cap_net_bind_service,cap_sys_time
USER         PID COMMAND
chrony       850 chronyd

Audit services for capability sets

grep CapEff /proc/*/status with capsh --decode shows which processes still hold broad masks such as 000001ffffffffff. systemd-analyze security UNIT scores a unit's sandboxing, including its capability bounding set.


Interview Checkpoints

L1: What are Linux capabilities, and why do they exist?

Say first: they split root's privileges into separate bits, so a process gets only the ones it needs instead of full root.

Proof: sudo capsh --drop=cap_chown -- -c 'chown ...' failed even as root.

Follow-up: Which capabilities are nearly equal to root?

L1: How does ping send ICMP without being setuid root?

Say first: on Ubuntu it carries the file capability cap_net_raw; on RHEL it uses unprivileged ICMP sockets allowed by net.ipv4.ping_group_range.

Proof: getcap /usr/bin/ping; sysctl net.ipv4.ping_group_range.

Follow-up: Why is a file capability safer than setuid root?

L2: Let a service running as a normal user listen on port 443.

Say first: grant CAP_NET_BIND_SERVICE in the unit, not on a shared binary.

Proof: AmbientCapabilities=CAP_NET_BIND_SERVICE and CapabilityBoundingSet=CAP_NET_BIND_SERVICE; grep CapEff /proc/PID/status shows 0000000000000400.

Follow-up: What are the alternatives (sysctl, a proxy, socket activation)?

L2: Read a process's capabilities and decode them.

Say first: read the masks from /proc/PID/status and decode with capsh.

Proof: grep CapEff /proc/$(pgrep -x chronyd)/status; capsh --decode=0000000002000400 prints cap_net_bind_service,cap_sys_time.

Follow-up: Which set does the kernel check at the moment of a system call?

L3: A binary with setcap worked yesterday; after a deployment it gets Permission denied binding port 99. What happened?

Say first: the deployment replaced or copied the file, and the capability in its extended attribute was lost.

Proof: getcap on the new file prints nothing; cp did the same in the capture; reapply setcap in the deployment, or move the grant into the systemd unit.

Follow-up: Which filesystems or mounts ignore file capabilities?

L4: A container runs as root. Why can it not mount a filesystem or load a kernel module?

Say first: the runtime drops most capabilities from the bounding set, so the root user inside lacks CAP_SYS_ADMIN and CAP_SYS_MODULE.

Proof: grep CapBnd /proc/1/status inside the container shows a reduced mask; capsh --decode lists the default set.

Don't say: root in a container is the same as root on the host.


Captured on Ubuntu 24.04.4 (libcap 2.66) and Rocky Linux 10.2 on iximiuz Labs FlexBox microVMs, kernel 6.1.167, 2026-09.