<aside> ๐งญ
Module 13 ยท Isolation: capabilities, seccomp and hardening
Module 12 built a container that could see almost nothing โ and was still running as root with every privilege the kernel offers. This module is the other half: how root was broken into 41 separate privileges, how seccomp removes system calls outright, and why one flag undoes all of it.
๐ง concept โ ๐งฉ real-world analogy โ ๐งช exercise โ โ expected result (hidden) โ ๐ฏ interview questions (hidden)
</aside>
<aside> โ
Before you start, you should already know:
From Module 02 โ fork, exec, and that exec replaces the program inside an existing process.
From Module 03 โ file permissions, owners, and what a mount option is.
From Module 04 โ signals, and that a signal can kill a process.
From Module 12 โ namespaces, cgroups, and that "a container" is just a process with some of them applied.
Tools used here: libcap2-bin (capsh, getcap, setcap, getpcaps), util-linux (setpriv), and attr (getfattr). Install with sudo apt-get install -y libcap2-bin attr on Debian/Ubuntu, or sudo dnf install -y libcap libcap-ng-utils attr on RHEL-family.
</aside>
<aside> ๐
Official docs: capabilities(7) ยท capsh(1) ยท proc_pid_status(5)
</aside>
For most of UNIX history, privilege was one bit. If your effective UID was 0, the kernel skipped its permission checks โ all of them. If it was anything else, it did not. There was nothing in between.
That created a problem that has nothing to do with security policy and everything to do with plumbing. ping needs to open a raw network socket, which ordinary users may not do. The only way to give ping that one ability was to make it setuid root โ and a setuid-root binary does not get "the ability to open raw sockets", it gets everything. One bug in ping is then a bug that can rewrite /etc/shadow.
Linux fixed this by splitting root into separate, independently grantable privileges called capabilities. There are 41 of them today, numbered 0 to 40. ping needs exactly one of them.
<aside> ๐ง
The counter-intuitive part, and the point of the whole section. "Root" is no longer a thing the kernel checks. When a process asks to change a file's owner, the kernel does not ask "is your UID 0?" โ it asks "do you hold CAP_CHOWN?" UID 0 normally comes with all 41 capabilities, which is why the two look identical from the outside. Take one capability away and a UID-0 process fails at that one operation while remaining root for everything else. You will do exactly this in Exercise A1.2.
</aside>
The count is not a number to memorise โ it is a file you read:
| Capability | What it lets a process do | Why you meet it |
|---|---|---|
CAP_CHOWN |
Change any file's owner | In Docker's default set; needed by package installs |
CAP_DAC_OVERRIDE |
Ignore file read/write/execute permission bits | The one that makes root "able to read anything" |
CAP_NET_BIND_SERVICE |
Bind a port below 1024 | The reason web servers used to start as root |
CAP_NET_RAW |
Raw and packet sockets | ping, tcpdump โ and ARP/DNS spoofing from a compromised container |
CAP_NET_ADMIN |
Interfaces, routes, firewall rules | Anything that configures networking; CNI plugins |
CAP_SYS_ADMIN |
Mount, pivot_root, and a long tail of unrelated things |
The junk drawer โ see the warning below |
CAP_SYS_PTRACE |
Attach a debugger to another process | strace, gdb, and reading another process's memory |
CAP_SYS_TIME |
Set the system clock | NTP daemons |
CAP_SYS_MODULE |
Load and unload kernel modules | Effectively equal to full root โ a module runs in the kernel |
CAP_MKNOD |
Create device nodes | In Docker's default set; combined with a host mount it is dangerous |
CAP_SETUID / CAP_SETGID |
Change UID/GID arbitrarily | How a service drops to an unprivileged user after start |
CAP_SETFCAP / CAP_SETPCAP |
Set capabilities on files / change your own bounding and inheritable sets | SETFCAP writes file capabilities; SETPCAP is what lets a process shrink its own bounding set โ it is how capsh --drop and container runtimes work |
CAP_BPF, CAP_PERFMON |
Load BPF programs; use perf |
Added in 5.8 so observability tools stop needing CAP_SYS_ADMIN |
CAP_CHECKPOINT_RESTORE |
Checkpoint and restore a process | Added in 5.9; currently the highest-numbered capability, number 40 |
<aside> โ ๏ธ
CAP_SYS_ADMIN is not a capability, it is a junk drawer. capabilities(7) carries an unusual note addressed to kernel developers: "Don't choose CAP_SYS_ADMIN if you can possibly avoid it!" โ because, in its words, "a vast proportion of existing capability checks are associated with this capability."
How vast: Michael Kerrisk counted it in CAP_SYS_ADMIN: the new root (LWN, March 2012). In the Linux 3.2 source, 451 of 1167 capability checks were CAP_SYS_ADMIN โ roughly 38%. A comment on that article puts it just over 45% by Linux 5.2. Treat those as historical measurements of a trend, not a current figure: nobody re-counts them each release.
What this means for you. --cap-add=SYS_ADMIN on a container is not "one more capability". It is mount, pivot_root, setns, quota control and a hundred unrelated operations, several of which are documented escape routes out of a container. When a vendor's install guide asks for it, that is the moment to ask which single operation they actually need.
</aside>
<aside> ๐งฉ
Real-world analogy โ the master key and the key ring
An old office building has one master key. It opens the front door, every office, the server room, the safe and the boiler room. There are exactly two kinds of person: someone with the master key, and someone without.
Now the boiler needs servicing. The contractor needs the boiler room โ and the only key that exists is the master. So you hand over the master key for the afternoon and hope. That is setuid root.
Capabilities are the day the building manager cut the master key into 41 separate keys on a ring. The contractor gets the boiler-room key and nothing else. The window cleaner gets the roof key. Nobody carries the safe key unless they are opening the safe.
And the detail people miss: the manager's own ring is just all 41 keys. There is no longer a "manager key" that works by magic. Take the safe key off the manager's ring and the manager cannot open the safe โ while still opening every other door in the building. That is UID 0 without CAP_CHOWN.
One key on that ring is labelled "misc", and over the years it quietly became the key to about a third of the doors, because reusing it was easier than cutting a new one. That is CAP_SYS_ADMIN.
Where the analogy stops working. A key works from the moment you hold it until you hand it back. A capability is checked per operation, at the moment of the operation, and is normally discarded when the process runs a different program โ closer to a key that dissolves as you walk through the door.
</aside>
๐งช Exercise A1.1 โ Take an inventory of privilege on your own machine
# 1. How many capabilities does THIS kernel know about?
# The file holds the highest number, so the count is that + 1.
cat /proc/sys/kernel/cap_last_cap
# 2. Name every one of them
capsh --print | sed -n 's/^Bounding set =//p' | tr ',' '\n' | head -5
capsh --print | sed -n 's/^Bounding set =//p' | tr ',' '\n' | wc -l
# 3. What does your ordinary shell hold?
grep -E '^Cap(Inh|Prm|Eff|Bnd|Amb)|^NoNewPrivs|^Seccomp:' /proc/$$/status
# 4. And a root process?
sudo grep -E '^Cap(Prm|Eff|Bnd)' /proc/self/status
# 5. Hex masks are unreadable. Turn one into names.
capsh --decode=0000000000003000
capsh --decode=000001ffffffffff | tr ',' '\n' | wc -l
# 6. Which programs on this machine carry capabilities in the filesystem?
sudo getcap -r /usr/bin /usr/sbin /bin 2>/dev/null
# ...and the old way of doing the same job:
find /usr/bin /usr/sbin -perm -4000 -type f 2>/dev/null | head -6
<aside> ๐ญ
Now imagine this at 500 hosts. sudo getcap -r / 2>/dev/null and find / -perm -4000 -type f are two of the cheapest fleet-wide audits you can run, and both belong in your configuration baseline. A binary that gained cap_setuid=ep or a new setuid bit since the last run is either a package update or an intrusion, and you want to know which within the hour. The output is short enough to diff.
</aside>