$RodHat_
Console Tips

Linux capabilities: what the five sets actually do

Published by · Updated

Linux capabilities: what the five sets actually do
Photo: AI-generated — no human photographer / RodHat AI Cover

The man page for capabilities(7) is 3,000 words and still manages to leave you confused about the exec rules. This is the part people skip. It is also the part that causes setuid binaries, capability leaks, and containers that silently run with more privilege than intended.

There are five per-thread capability sets. You need to understand all five.

The five sets

Permitted: the ceiling. The process can raise any cap into effective only if it is already in permitted. Once a cap is dropped from permitted, it cannot be regained without executing a binary that has file capabilities granting it (or a setuid-root binary, if you left that door open). There is no back door. Think of permitted as what the process is allowed to use.

Effective: what the kernel actually checks. System calls consult the effective set, not permitted. A process can hold CAP_NET_BIND_SERVICE in permitted without it being active in effective. Network daemons that call capset to raise and drop caps around the bind() call are toggling effective, not permitted.

Inheritable: capabilities that can survive execve. The inheritable set on its own does nothing for an unprivileged process at exec. Its interaction with the file’s inheritable set and the ambient set is where the complexity lives.

Bounding: an upper limit on what file capabilities can grant at exec. You can only shrink the bounding set, via prctl(PR_CAPBSET_DROP, ...), and only while you hold CAP_SETPCAP. Once a cap is out of the bounding set it never comes back. A capability not in the bounding set cannot end up in the new process’s permitted set via file capabilities, regardless of what the file says. The bounding set is also inherited across fork and survives exec unchanged.

Ambient: added in Linux 4.3. Automatically carried into any exec, including binaries with no file capabilities. If a cap is in the ambient set, it ends up in the new process’s permitted and effective sets. The constraint: a cap can only be in ambient if it is already in both permitted and inheritable.

The exec transformation

This is the part the guides hand-wave. At execve, the kernel computes the new process’s sets with these rules. P is the thread before exec, F is the file being executed, P’ is the new process:

P'(ambient)     = (file has no caps) ? P(ambient) : 0

P'(permitted)   = (P(inheritable) & F(inheritable))
                | (F(permitted) & P(bounding))
                | P'(ambient)

P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

P'(inheritable) = P(inheritable)
P'(bounding)    = P(bounding)

The F(effective) in the effective rule is a single bit on the file, not a set. If it is set, all of P’(permitted) becomes P’(effective). This is the old model: the binary gets everything it was granted and manages privilege itself. If it is clear, the binary starts with an empty effective set and is expected to call capset to raise specific caps from permitted into effective as needed.

Practically: most setcap invocations set the +eip flags, which sets effective. The +ip without e is the explicit-activation model, and it is the right one for security-conscious code. Your daemon grabs what it needs, drops the rest.

For a non-root process exec-ing a file with no file capabilities and no ambient caps, P’(permitted) and P’(effective) are both empty. That is the default and correct outcome when you want a subprocess that has no capabilities at all.

Ambient capabilities in practice

Before Linux 4.3, there was no clean way to pass capabilities to a child process executing a plain binary. Your options were: make the binary setuid, put file capabilities on the binary with setcap, or use a wrapper that runs as root and calls capset in the child before exec (which requires the parent to be privileged enough to call capset on a different thread). None of these are good.

Ambient caps let you do this without touching the file:

#include <sys/prctl.h>
#include <sys/capability.h>

/* Parent: set up ambient before fork/exec */

/* The cap must be in permitted first */
cap_t caps = cap_get_proc();
cap_value_t cap_list[] = { CAP_NET_BIND_SERVICE };
cap_set_flag(caps, CAP_INHERITABLE, 1, cap_list, CAP_SET);
cap_set_proc(caps);
cap_free(caps);

/* Now raise it into ambient */
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE,
          CAP_NET_BIND_SERVICE, 0, 0) < 0) {
    perror("PR_CAP_AMBIENT_RAISE");
    exit(1);
}

/* fork + exec the child binary — it gets CAP_NET_BIND_SERVICE
   in its permitted and effective sets, with no file caps needed */

Check whether a specific cap is in the ambient set:

int r = prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET,
              CAP_NET_BIND_SERVICE, 0, 0);
/* 1 if set, 0 if not, -1 on error */

Clear the entire ambient set:

prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);

The ambient set is cleared on execve if the file has any file capabilities. It is also cleared whenever you drop the corresponding cap from permitted or inheritable. The kernel enforces the invariant that ambient is always a subset of permitted ∩ inheritable.

Dropping capabilities correctly

Use libcap, not raw capset(2). Raw capset manipulates __user_cap_header_struct and __user_cap_data_struct by hand, and the header version field has bitten people with the 64-bit capability set extension. libcap handles this.

Drop all caps:

cap_t empty = cap_init();
if (cap_set_proc(empty) < 0) {
    perror("cap_set_proc");
    exit(1);
}
cap_free(empty);

cap_init() returns a set with all three traditional sets (permitted, effective, inheritable) cleared. It does not touch ambient or bounding. To clear ambient:

prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);

To shrink the bounding set below all caps, loop and call prctl(PR_CAPBSET_DROP, cap, ...) for each bit. CAP_SETPCAP is required. If you are already running as the service user without CAP_SETPCAP in effective, you cannot shrink the bounding set at runtime, but you can set it with file capabilities on the launcher binary or via the service manager.

#include <linux/capability.h>

for (int cap = 0; cap <= CAP_LAST_CAP; cap++) {
    if (prctl(PR_CAPBSET_READ, cap, 0, 0, 0) == 1) {
        if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) < 0) {
            /* EPERM if we lack CAP_SETPCAP — log and continue */
            perror("PR_CAPBSET_DROP");
        }
    }
}

File capabilities

setcap and getcap from the libcap2-bin package manage file capabilities stored in the security.capability xattr.

# Grant the binary CAP_NET_BIND_SERVICE in permitted and effective
sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myserver

# Verify
getcap /usr/local/bin/myserver
# myserver cap_net_bind_service=ep

# Clear all file caps
sudo setcap -r /usr/local/bin/myserver

The =ep means: permitted and effective, no inheritable. The exec transformation then puts CAP_NET_BIND_SERVICE into P’(permitted) ∩ P(bounding), and because effective is set, also into P’(effective).

The binary now binds port 80 without being setuid and without the parent needing any capabilities. It should drop CAP_NET_BIND_SERVICE from effective immediately after the bind() call, and drop it from permitted before doing anything else. Holding it through the process lifetime is the mistake.

securebits

There is a third prctl knob that matters here: PR_SET_SECUREBITS. The bitmask controls what happens to capabilities when UID transitions occur:

  • SECBIT_NOROOT: when set, UID 0 does not automatically get all capabilities. This is the bit that makes the capability model coherent; without it, exec-ing a setuid-root binary grants full capability sets and the whole system you built is bypassed.
  • SECBIT_NO_SETUID_FIXUP: disables the automatic adjustment of effective and permitted sets when a thread’s UID transitions to or from zero.
  • SECBIT_KEEP_CAPS: preserves permitted caps across a switch of all UIDs to non-zero. Effective still gets wiped when the effective UID goes non-zero; you re-raise what you need from permitted afterwards. Needed when a daemon drops to a service UID but still needs to toggle caps afterward. Cleared on every execve. prctl(PR_SET_KEEPCAPS, 1) is the older spelling of the same bit.

For a daemon that drops privileges and wants no surprises:

#include <sys/prctl.h>
#include <linux/securebits.h>

prctl(PR_SET_SECUREBITS,
      SECBIT_NOROOT | SECBIT_NOROOT_LOCKED |
      SECBIT_NO_SETUID_FIXUP | SECBIT_NO_SETUID_FIXUP_LOCKED,
      0, 0, 0);

The _LOCKED variants prevent the corresponding bit from being unset later, even by a privileged thread.

Inspecting caps from the shell

/proc/PID/status has five hex fields:

CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

Decode the hex with capsh --decode:

capsh --decode=000001ffffffffff
# 0x000001ffffffffff=cap_chown,cap_dac_override,...,cap_checkpoint_restore

To inspect a running process by PID:

cat /proc/$(pgrep myserver)/status | grep Cap
capsh --decode=$(grep CapEff /proc/$(pgrep myserver)/status | awk '{print $2}')

Or with getpcaps from libcap2-bin:

getpcaps $(pgrep myserver)
# myserver (pid 12345): cap_net_bind_service=ep

Sequence for a privilege-dropping daemon

void harden_caps(void) {
    /* 1. Do privileged work (bind ports, open raw sockets, etc.) */
    bind_privileged_ports();

    /* 2. Set securebits to prevent UID-0 bypass (needs CAP_SETPCAP).
          NO_SETUID_FIXUP means the UID switch below leaves the cap
          sets alone, so we still hold CAP_SETPCAP for step 4. Without
          it, set SECBIT_KEEP_CAPS here instead, and re-raise effective
          from permitted after the switch. */
    if (prctl(PR_SET_SECUREBITS,
              SECBIT_NOROOT | SECBIT_NOROOT_LOCKED |
              SECBIT_NO_SETUID_FIXUP | SECBIT_NO_SETUID_FIXUP_LOCKED,
              0, 0, 0) < 0) {
        perror("PR_SET_SECUREBITS");
        exit(1);
    }

    /* 3. Drop to service UID/GID while we still hold CAP_SETGID and
          CAP_SETUID. Do this after dropping caps and every one of
          these fails with EPERM and you're still root. */
    if (setgroups(0, NULL) < 0 ||
        setresgid(SERVICE_GID, SERVICE_GID, SERVICE_GID) < 0 ||
        setresuid(SERVICE_UID, SERVICE_UID, SERVICE_UID) < 0) {
        perror("drop uid/gid");
        exit(1);
    }

    /* 4. Empty the bounding set (still needs CAP_SETPCAP) */
    for (int cap = 0; cap <= CAP_LAST_CAP; cap++)
        if (prctl(PR_CAPBSET_READ, cap, 0, 0, 0) == 1)
            prctl(PR_CAPBSET_DROP, cap, 0, 0, 0);

    /* 5. Clear ambient, then drop all caps except what is still needed */
    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
    cap_t caps = cap_init();
    /* If you need nothing past this point, cap_init() is already all-zeros */
    if (cap_set_proc(caps) < 0) {
        perror("cap_set_proc");
        exit(1);
    }
    cap_free(caps);

    /* 6. Lock down: no new privs, proc not dumpable */
    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
}

PR_SET_NO_NEW_PRIVS and PR_SET_DUMPABLE are from the prctl post. Seccomp goes on after NO_NEW_PRIVS is set, per the seccomp-bpf post. Landlock goes alongside seccomp to restrict filesystem access, per the landlock post.

The common mistake: calling cap_set_proc with an empty set but forgetting to set securebits first. A subsequent exec of a setuid binary restores full root capabilities unless SECBIT_NOROOT is locked. The locks matter.

Kerrisk covers this in exhaustive detail in Chapter 39 of The Linux Programming Interface. It is the most useful 80 pages on Linux capabilities in print, including the exec transformation table that this post summarizes. If you are writing privilege-dropping code and you have not read Chapter 39, read it before shipping.