Skip to main content
RunBook Academy

LinuxXXX · Linux Capabilities and PrivilegeProcess capabilities

Process capability sets and the exec transition

Advanced⏱ ~16 mincapshgetpcaps

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

Not yet marked complete on this device.

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:

Read-only / SafeAn ordinary login shell: everything empty except the bounding set
$ grep -E '^Cap|^NoNewPrivs' /proc/self/status
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	000001ffffffffff
CapAmb:	0000000000000000
NoNewPrivs:	0
SetFieldWhat it is
PermittedCapPrmThe capabilities the thread may raise into effective
EffectiveCapEffThe capabilities the kernel actually checks against right now
InheritableCapInhCapabilities that may be preserved across exec, if the file agrees
BoundingCapBndA ceiling: nothing outside it can ever be gained
AmbientCapAmbCapabilities 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:

  1. 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+i on a binary appears to do nothing when you run it from a normal shell whose CapInh is empty.
  2. The file grant. The capability is in the file’s permitted set and is still inside the thread’s bounding set. This is the +ep case from the file capabilities lesson. The bounding set is why a capability granted on a file can still fail to appear.
  3. 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) ? 0 term, and it stops ambient capabilities leaking into a setuid program.
  • The SECBIT_NO_CAP_AMBIENT_RAISE securebit 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.

Configuration changecapsh needs cap_setuid and cap_setgid to perform --user itself
# 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:	0000000000000400

Illustrative 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:

Read-only / SafeDefaults on a normal host
$ 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-noroot disables the special treatment of UID 0 in the transition rules, so root is no longer automatically granted everything at exec.
  • secure-no-suid-fixup stops capabilities being recalculated when UIDs change.
  • no_new_privs is separate from all of these. It blocks privilege gain across execve(): 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:

TermValue
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

  1. 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?

  2. Q2. What does the bounding set do during the exec transition?

  3. Q3. Which conditions must hold for a capability to be raised into the ambient set? Select all that apply.

  4. Q4. Executing a setuid binary clears the ambient capability set of the resulting process.

  5. 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.