LinuxXXX · Linux Capabilities and PrivilegeProcess capabilities
Process capability sets and the exec transition
What you'll learn
- Name the five per-thread capability sets and what each controls
- Apply the execve transition algorithm to predict a process capability set
- Explain why an inheritable capability alone grants nothing
- Use the ambient set to carry a capability across exec
- Diagnose an EPERM caused by a capability-dumb binary
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-11
The earlier lessons in this part put capabilities on files and
read them back off processes. This one covers the arithmetic in
between: the kernel takes five sets belonging to the calling
thread and three belonging to the file, and computes the new
sets at execve().
Knowing that calculation is the difference between “I set the capability and it did not work” and knowing which of the five inputs was zero.
The five per-thread sets
Every thread carries five capability bitmasks. /proc/PID/status
prints all of them:
$ grep -E '^Cap|^NoNewPrivs' /proc/self/statusCapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0| Set | Field | What it is |
|---|---|---|
| Permitted | CapPrm | The capabilities the thread may raise into effective |
| Effective | CapEff | The capabilities the kernel actually checks against right now |
| Inheritable | CapInh | Capabilities that may be preserved across exec, if the file agrees |
| Bounding | CapBnd | A ceiling: nothing outside it can ever be gained |
| Ambient | CapAmb | Capabilities carried across exec of an unprivileged file |
Two of these are frequently misread:
- Inheritable is not “the child gets this.” On its own it grants nothing at all. It is one half of a handshake with the file.
- Bounding is not a grant. It is a mask. A capability in the bounding set and nowhere else is not held by anything.
The file carries three of its own, set by setcap: a permitted
set, an inheritable set, and a single effective bit.
The transition algorithm
At execve() the kernel computes the new sets, written P',
from the thread’s current sets P and the file’s sets F:
P'(ambient) = (file is privileged) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & P(bounding)) |
P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable) [unchanged]
P'(bounding) = P(bounding) [unchanged]
“File is privileged” means the file has capabilities, or has the set-user-ID or set-group-ID bit set.
Read the permitted line as three independent routes by which a capability can arrive:
- The handshake. The capability is in the thread’s
inheritable set and in the file’s inheritable set. Either
one alone contributes nothing - this is why
setcap cap_net_raw+ion a binary appears to do nothing when you run it from a normal shell whoseCapInhis empty. - The file grant. The capability is in the file’s
permitted set and is still inside the thread’s bounding set.
This is the
+epcase from the file capabilities lesson. The bounding set is why a capability granted on a file can still fail to appear. - The ambient carry. Whatever survived in
P'(ambient).
And the effective line says: a capability that is permitted is
not necessarily active. Either the file sets its effective
bit - the e in cap_net_raw=ep - or the program has to raise
it itself through libcap.
Why inheritable alone does nothing
This is the single most common capability misunderstanding, and the algorithm explains it in one line:
P'(permitted) includes (P(inheritable) & F(inheritable))
An & needs both operands. Your interactive shell has
CapInh: 0000000000000000. Anything ANDed with zero is zero.
So a binary marked cap_net_bind_service+i and run from a
normal shell gets nothing, and the operator concludes that
capabilities “do not work”.
Both halves have to be non-zero. Confirm which half is missing before changing anything:
# The file half
getcap /usr/local/bin/portbind
# The process half, for the shell you are about to run it from
capsh --decode="$(awk '/^CapInh:/ { print $2 }' /proc/self/status)"
capsh --inh= sets the process half, but only for a caller
that already holds the capability or CAP_SETPCAP; as an
ordinary user it fails with Unable to set inheritable capabilities: Operation not permitted, which is the kernel
telling you the same thing the algorithm does.
The inheritable route exists for a reason - it lets a
privileged launcher hand a capability to a specific binary that
has opted in with F(inheritable) - but it is a two-sided
arrangement that has to be configured on both sides. For
almost all service work the ambient set is the better tool.
The ambient set
The ambient set was added in Linux 4.3 to solve a real problem: there was no way to run an ordinary, unmarked binary with a capability. Before it, you either put capabilities on the file, which grants them to everyone who can execute the file, or you used setuid.
Ambient capabilities are carried across execve() of an
unprivileged file, and they land in both permitted and
effective. The rules that constrain them:
- A capability can only be raised into ambient if it is already in both the permitted and the inheritable sets.
- Dropping a capability from permitted or inheritable automatically drops it from ambient.
- Executing a privileged file - one with file capabilities
or a setuid/setgid bit - clears the entire ambient set. This
is the
(file is privileged) ? 0term, and it stops ambient capabilities leaking into a setuid program. - The
SECBIT_NO_CAP_AMBIENT_RAISEsecurebit can lock ambient raising off entirely.
By hand the sequence has to satisfy those rules in order: acquire the capability, keep it across the UID change, drop to the service user, then raise it into ambient.
# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' --keep=1 --user=appsvc --addamb=cap_net_bind_service -- -c 'grep -E "^CapPrm|^CapEff|^CapAmb" /proc/self/status'CapPrm: 0000000000000400
CapEff: 0000000000000400
CapAmb: 0000000000000400Illustrative output
--keep=1 is the part people leave out. Without it the kernel
clears the permitted set when the UID changes away from 0, and
--addamb then fails because ambient raising requires the
capability to still be permitted.
You almost never do this by hand. systemd does it for you:
[Service]
User=appsvc
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
systemd raises the ambient capability before dropping to
User=, so the service binds port 443 as appsvc with a
completely unmarked binary. Package upgrades cannot silently
remove it, because there is no xattr to remove.
Confirm what the running service actually got:
PID=$(systemctl show -p MainPID --value myservice)
grep -E '^Cap' /proc/"$PID"/status
capsh --decode="$(awk '/^CapAmb:/ { print $2 }' /proc/"$PID"/status)"
Securebits and no_new_privs
capsh --print shows a securebits line that most people scroll
past:
$ capsh --print | sed -n '/Securebits/,/secure-no-ambient-raise/p'Securebits: 00/0x0/1'b0 (no-new-privs=0)
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
secure-no-ambient-raise: no (unlocked)secure-norootdisables the special treatment of UID 0 in the transition rules, so root is no longer automatically granted everything at exec.secure-no-suid-fixupstops capabilities being recalculated when UIDs change.no_new_privsis separate from all of these. It blocks privilege gain acrossexecve(): file capabilities and setuid bits stop taking effect for that process and all its descendants, permanently.
no_new_privs and the bounding set are the two controls people
confuse. The bounding set decides what could ever be acquired;
no_new_privs decides whether the acquisition mechanisms work
at all. A service can have a generous bounding set and gain
nothing because NoNewPrivileges=true closed the routes.
Working through an example
A service binary at /usr/local/bin/portbind with
cap_net_bind_service=ep, executed from a systemd unit with
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN and no
ambient capabilities:
| Term | Value |
|---|---|
P(inheritable) | empty |
F(inheritable) | empty |
F(permitted) | cap_net_bind_service |
P(bounding) | cap_net_bind_service, cap_chown |
P(ambient) | empty |
F(effective) | set |
P'(ambient) is 0, because the file is privileged. The
handshake term is 0. The file grant term is
cap_net_bind_service & {net_bind_service, chown} =
cap_net_bind_service. So P'(permitted) is
cap_net_bind_service, and because the file’s effective bit is
set, P'(effective) is the same. The service binds port 443
and holds nothing else.
Now remove CAP_NET_BIND_SERVICE from the unit’s bounding set.
The file grant term becomes empty, P'(permitted) is empty,
and the capability-dumb check fires: the service fails to start
with EPERM and journalctl reports a permission error on the
executable.
Knowledge check
Knowledge check · 5 questions
Q1. A binary has `cap_net_raw+i` set with setcap. You run it from an ordinary login shell. What capabilities does the process get?
Q2. What does the bounding set do during the exec transition?
Q3. Which conditions must hold for a capability to be raised into the ambient set? Select all that apply.
Q4. Executing a setuid binary clears the ambient capability set of the resulting process.
Q5. A binary with `cap_sys_time=ep` fails to exec with EPERM under a systemd unit. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.