avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

Linux Capabilities Deep Dive: The Art of Process Privilege Control

Preface ​

In the previous article, we explored runc’s defense-in-depth security model from SPEC.md, but left a core question unanswered: Why must CapPrm and CapEff be separate if they’re both just collections of capabilities? And what’s the difference between CapBnd and them?

To truly answer these questions, we first need to understand two prerequisite concepts: fork and exec. This article takes a zero-to-hero approach, driven by hands-on experiments, to demystify all five attributes of Linux Capabilities.

I. Prerequisites: fork and exec ​

1.1 fork = Clone a Process ​

fork() is a Linux system call that creates an identical copy of the current process:

c
int pid = fork();

if (pid == 0) {
    // I am the child process
    printf("Child: PID=%d\n", getpid());
} else {
    // I am the parent process
    printf("Parent: child PID=%d\n", pid);
}

Execution result:

Parent: child PID=101
Child: PID=101

After fork, the parent and child processes are completely independent — they run separately and don’t interfere with each other.

1.2 exec = Replace the Soul ​

execve() is another system call that replaces the current process’s memory space with a new program:

c
// Currently a bash process
execve("/bin/ls", argv, envp);
// After this line, the process PID is still the same (e.g., 100),
// but it has become the ls program

Key point: The PID stays the same, but the running program changes.

1.3 fork + exec = Run a New Program ​

This is the classic Linux process creation pattern:

shell (PID=100)
  fork() → child process (PID=101, a clone of shell)
    execve("/bin/runc", ...) → PID=101 is now runc
      runc starts running

Docker/container process creation follows this pattern: fork first, then exec.

II. The Five Attributes of Capabilities ​

A Linux process’s capabilities consist of five sets, viewable via cat /proc/self/status:

CapInh: 0000000000000000   # Inheritable
CapPrm: 000001ffffffffff   # Permitted
CapEff: 000001ffffffffff   # Effective
CapBnd: 000001ffffffffff   # Bounding
CapAmb: 0000000000000000   # Ambient

2.1 CapEff (Effective) — Currently Active Capabilities ​

Capabilities the process is actively using right now.

Analogy: Tools in your hand.

2.2 CapPrm (Permitted) — Allowed Capabilities ​

The upper limit of capabilities a process may ever have.

Analogy: Tools in your bag. You don’t have to use them, but you have them.

2.3 CapBnd (Bounding) — System-Wide Ceiling ​

A system-level restriction defining what capabilities a process may ever have during its lifetime.

Analogy: Your highest education degree. You can go lower, never higher; once dropped, it cannot be recovered.

2.4 CapInh (Inheritable) — Inherited on fork ​

Which capabilities child processes inherit from the parent via fork().

Analogy: Genetic inheritance — child processes automatically receive it.

2.5 CapAmb (Ambient) — Preserved After exec ​

Capabilities that survive an execve() program replacement (only applies to non-root programs).

Analogy: A随身携带的证件 — still valid after swapping programs.

III. Why Must CapPrm and CapEff Be Separate? ​

This is the most elegant design decision in Capabilities — the Principle of Least Privilege.

3.1 Scenario 1: Web Server ​

An nginx process needs cap_net_bind_service to bind port 80, but 99% of the time it’s just handling normal HTTP requests and doesn’t need this capability at all.

At startup:
  CapPrm = cap_net_bind_service, cap_setuid, cap_sys_admin ...
  CapEff = cap_net_bind_service  ← briefly needed to bind port 80

After bind() completes:
  CapEff = empty  ← done, drop it

Processing 10,000 HTTP requests:
  CapEff = empty  ← normal operations don't need any privilege

When CGI script execution is needed:
  CapEff = cap_setuid  ← temporarily enabled
  Execution completes
  CapEff = empty  ← drop again

... repeat ...

Benefit: If an attacker compromises nginx, they only get whatever CapEff was at that moment. If CapEff were always equal to CapPrm (all capabilities), the attacker immediately has full power.

3.2 Scenario 2: setuid Program Privilege Drop ​

The sudo process lifecycle:

If CapEff and CapPrm were locked together, temporary privilege escalation would be very cumbersome.

3.3 Scenario 3: Fork Child Process Inheritance ​

Parent process:
  CapInh = {cap_net_bind}
  CapPrm = {cap_net_bind}
  CapEff = {cap_net_bind}

After fork():
  Child automatically inherits:
    CapInh = {cap_net_bind}
    CapPrm = {cap_net_bind} (inherited from parent)
    CapEff = {cap_net_bind}

Child immediately drops CapEff:
    CapPrm = {cap_net_bind} (still in the bag)
    CapEff = empty

→ If child process is exploited by an attacker, they get CapEff = empty
  instead of CapPrm = {cap_net_bind}

3.4 One-Line Summary ​

CapPrm is the "ammunition depot", CapEff is the "gun in hand". The depot can be stolen, but without the gun in hand nothing can be fired. Separation ensures CapEff stays minimal or empty at all times — even when attacked, only minimal privileges are exposed.

IV. CapBnd vs CapPrm ​

These two are the most commonly confused:

CapBndCapPrm
ScopeSystem-level, global restriction on entire process treeProcess-level, individual process restriction
RecoveryNo, dropped capabilities are gone forever, even after execYes, capset() can re-enable anytime
Initial valueKernel’s cap_last_cap defines maximum (usually all 40)Inherited from parent on fork/exec

4.1 Analogy ​

CapBnd = Your "highest education degree" (PhD)
         → The maximum you've ever achieved, cannot go higher
         → Even if you drop it, you can never get it back (unless reboot)

CapPrm = "Certificates in your bag" (engineer license + teacher license + ...)
         → Can discard anytime, can earn new ones to replace

4.2 CapBnd’s Real Value: Preventing Privilege Escalation ​

CapBnd is the last line of defense against privilege escalation by already-capable processes.

Docker container escape scenario:

Host runc process:
  CapBnd = full (all 40 capabilities)
  CapPrm = full
  CapEff = full

When creating a container, runc trims the container's CapBnd (removing cap_sys_admin, etc.):
  Container process:
    CapBnd = {without CAP_SYS_ADMIN}     ← kernel-enforced!
    CapPrm = {without CAP_SYS_ADMIN}
    CapEff = {without CAP_SYS_ADMIN}

  → Even if container process escalates via vulnerability, it cannot
    execute mount()/unshare()/chroot()
    because these require CAP_SYS_ADMIN which is not in CapBnd

CapBnd is the iron gate; CapPrm is the wooden door.

4.3 Operational Comparison ​

CapBnd can only be dropped, never recovered:

bash
capsh --drop=cap_sys_admin --

capsh --add=cap_sys_admin --print

CapPrm can be dropped and recovered:

bash
capsh --drop=cap_net_bind_service --

capsh --add=cap_net_bind_service --

V. CapInh vs CapAmb ​

CapInhCapAmb
Triggerfork inheritanceexec preservation
PurposeControls what children getControls what survives exec
Typical useForking privileged childrenMaintaining privilege across exec

5.1 How CapInh Works ​

Parent process:
  CapInh = {cap_net_bind_service}
  fork()
    ↓ child inherits
  Child CapInh = {cap_net_bind_service} (automatic)
  Child CapPrm = {cap_net_bind_service} (inherited from CapInh)

5.2 How CapAmb Works ​

Normal programs lose all capabilities after exec:

Normal process (no root):
  execve("/bin/ls", ...)
    → Loses all capabilities (because ls is not setuid)

CapAmb allows some to survive exec:

Normal process:
  CapPrm = {cap_net_bind_service}
  CapEff = {cap_net_bind_service}
  CapAmb = {cap_net_bind_service}  ← marked "preserve after exec"

  execve("/bin/myprogram", ...)
    → myprogram still has cap_net_bind_service

5.3 CapAmb’s Three Restrictions ​

  1. The program must not be setuid/setgid
  2. CapAmb ≤ CapPrm (cannot preserve what isn’t in the bag)
  3. CapAmb ≤ CapInh (cannot preserve what isn’t marked inheritable)

VI. Real-World Cases of CapInh and CapAmb ​

6.1 CapInh Case: Nginx Forking Worker Processes ​

nginx master process (root):
  CapInh = {cap_net_bind_service}  ← marked for inheritance
  fork()
    ↓ child inherits
  nginx worker process:
    CapPrm = {cap_net_bind_service} (inherited from CapInh)
    CapEff = {cap_net_bind_service}

6.2 CapAmb Case: Normal User Running Network Service ​

Normal user process:
  CapAmb = {cap_net_bind_service}
  execve("/usr/sbin/my-server", ...)
    → my-server can still bind port 80 (even without root)

VII. Full Initialization Sequence ​

Process startup (exec):
  CapBnd = kernel's cap_last_cap determines maximum set (usually all 40)
         ↓
  CapPrm = inherited from parent
         ↓
  CapEff = inherited from parent
         ↓
  CapInh  = inherited from parent (can be explicitly modified)
         ↓
  CapAmb  = inherited from parent (can be explicitly modified)

VIII. Common Commands for Viewing and Manipulating Capabilities ​

8.1 View current process capabilities ​

bash
cat /proc/self/status | grep Cap

Example output:

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

8.2 Decode hexadecimal to readable list ​

bash
sudo apt install libcap-ng-utils

capsh --decode=000001ffffffffff

Example output:

0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,
cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,
cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,
cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,
cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,
cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap ...

8.3 capsh tool reference ​

bash
capsh --print

capsh --drop=cap_sys_admin --print

capsh --add=cap_net_bind_service --print

capsh --caps="cap_net_bind_service+ep" -- -c "..."

8.4 View any process’s capabilities ​

bash
cat /proc/1234/status | grep Cap

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

8.5 getpcaps tool (view other processes) ​

bash
sudo apt install libcap-ng-utils

getpcaps 1234

IX. Hands-On Experiments ​

9.1 Experiment 1: Compare root vs normal user capabilities ​

bash
sudo cat /proc/$/status | grep Cap

su - testuser -c 'cat /proc/self/status | grep Cap'

Compare: root’s CapEff is usually full (000001ffffffffff), while normal users typically have only a subset.

9.2 Experiment 2: CapBnd cannot be recovered after drop ​

bash
capsh --drop=cap_sys_admin --

capsh --add=cap_sys_admin --print

9.3 Experiment 3: CapInh inheritance across fork ​

In an environment with root privileges:

c
// fork_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/capability.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child: CapEff=%016llx\n", (unsigned long long)cap_get_flag(cap_get_proc(), CAP_EFFECTIVE));
        _exit(0);
    } else {
        wait(NULL);
    }
    return 0;
}

9.4 Experiment 4: CapAmb preserved after exec ​

As a normal user (non-root):

bash

sudo setcap cap_net_bind_service=ep /usr/sbin/python3

su - testuser -c "capsh --caps='cap_net_bind_service=ep' --addamb=cap_net_bind_service -- -c 'getpcaps \$\$'"

9.5 Experiment 5: Observe Docker container’s CapBnd ​

bash
docker run --rm ubuntu cat /proc/self/status | grep Cap

cat /proc/self/status | grep Cap

Container CapBnd typically has many fewer capabilities than the host (e.g., no CAP_SYS_ADMIN).

X. Capabilities in runc ​

When runc creates a container, it sets the container process’s capabilities based on the linux.capabilities field in config.json:

go
// libcontainer/capabilities/capabilities.go
func (c *linuxCap) ApplyCaps() error {
    // 1. Drop all capabilities
    for _, cap := range capability.List() {
        cap.Set(cap.Effective, false)
        cap.Set(cap.Permitted, false)
        cap.Set(cap.Inheritable, false)
    }

    // 2. Enable only capabilities declared in config.json
    for _, cap := range c.bounds {
        cap.Set(cap.Permitted, true)
        cap.Set(cap.Effective, true)
    }

    return nil
}

The container’s CapBnd is also trimmed — removing dangerous capabilities the container doesn’t need (like CAP_SYS_ADMIN, CAP_SYS_MODULE, etc.) to prevent container escape via these capabilities.

XI. Seccomp Deep Dive: Capabilities’ Companion ​

11.1 What is Seccomp ​

Seccomp (Secure Computing Mode) is a Linux kernel system call filter that restricts which system calls (syscalls) a process can invoke.

Every program interacts with the kernel via syscalls:

User program ──read()──→ kernel
User program ──write()──→ kernel
User program ──mount()──→ kernel  ← dangerous!
User program ──unshare()──→ kernel  ← dangerous!

Seccomp’s role is to intercept syscalls before they reach the kernel handler:

Without Seccomp:
  Program ──mount──→ ✅ Passes through  ← dangerous!

With Seccomp (only allows read/write/exit):
  Program ──mount──→ ❌ Rejected (returns -1, errno = EPERM)

11.2 Two Seccomp Modes ​

Mode 1 (Strict Mode, since kernel 2.6.12)

All-or-nothing, inflexible:

bash
echo 1 > /proc/self/seccomp

Mode 2 (Filter Mode, since kernel 3.5)

Allows rule-based syscall filtering — this is the mainstream approach:

Allowed: read, write, exit, sigreturn
Blocked: mount, unshare, mknod, chroot, ...

11.3 The Relationship Between Capabilities and Seccomp ​

They do overlap functionally, but have different design goals:

CapabilitiesSeccomp
GranularityIndividual capabilityIndividual system call
LayerLinux userspace permission modelKernel-enforced
Relationship"Do you have this license?""Are you allowed to call this syscall?"

Key difference:

Capabilities check happens INSIDE the syscall handler (allowed if you have the license)
Seccomp filter intercepts BEFORE the syscall reaches the handler

Both together provide double insurance:

Attacker exploits vulnerability and gains CAP_SYS_ADMIN:

With Capabilities only:
  mount() ──→ has CAP_SYS_ADMIN ──→ Success ⚠️ Container escape!

With Capabilities + Seccomp:
  mount() ──→ Seccomp intercepts ──→ EPERM ❌
              Even with CAP_SYS_ADMIN, it doesn't work

Analogy:

Capabilities = Security guard (checks your pass)
Seccomp      = Door lock (physically prevents opening)

11.4 Viewing and Configuring Seccomp ​

Check process Seccomp status:

bash
cat /proc/self/status | grep Seccomp

Enable Seccomp (Mode 2):

c
#include <linux/seccomp.h>
#include <sys/prctl.h>

// Enable Seccomp filter mode
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

Write rules with libseccomp:

c
#include <seccomp.h>

int main() {
    // Create filter: allow all by default
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

    // Block mount
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
                     SCMP_SYS(mount), 0);

    // Block unshare
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
                     SCMP_SYS(unshare), 0);

    // Load rules
    seccomp_load(ctx);

    // Now mount() is rejected
    mount("none", "/tmp", "tmpfs", 0, NULL);  // Returns EPERM
}

Verify Seccomp interception:

bash
strace mount -t tmpfs none /tmp 2>&1

Seccomp in runc (config.json):

json
{
  "linux": {
    "seccomp": {
      "defaultAction": "SCMP_ACT_ALLOW",
      "syscalls": [
        {
          "names": ["mount", "umount2", "unshare", "chroot", "mknod"],
          "action": "SCMP_ACT_ERRNO"
        }
      ]
    }
  }
}

XII. How to Configure Capability for a Process ​

12.1 Check Process Capability ​

bash
cat /proc/self/status | grep Cap

getpcaps <pid>

12.2 Set Capability on Executable File (Permanent) ​

Use setcap to set capability on a binary file:

bash

sudo setcap cap_net_bind_service=ep /usr/bin/python3

sudo setcap cap_net_raw=ep /bin/ping

sudo setcap cap_net_bind_service,cap_net_admin=ep /usr/sbin/myapp

getcap /usr/bin/python3

sudo setcap -r /usr/bin/python3

12.3 Adjust Capability on Running Process (Temporary) ​

Use capsh:

bash
capsh --drop=cap_sys_admin --print

capsh --caps="cap_net_bind_service+ep" -- -c "python3 -m http.server 80"

capsh --add=cap_net_bind_service --

12.4 Set Capability in Code (libcap) ​

c
#include <sys/capability.h>

int main() {
    // Get current process capabilities
    cap_t caps = cap_get_proc();

    // Clear all capabilities
    cap_clear(caps);

    // Add cap_net_bind_service to Effective and Permitted
    cap_value_t vals[] = {CAP_NET_BIND_SERVICE};
    cap_set_flag(caps, CAP_EFFECTIVE, 1, vals, CAP_SET);
    cap_set_flag(caps, CAP_PERMITTED, 1, vals, CAP_SET);

    // Apply to current process
    cap_set_proc(caps);

    // Now can bind port 80
    bind(sock, ...);

    cap_free(caps);
    return 0;
}

Compile:

bash
gcc -o myapp myapp.c -lcap

12.5 Configure in Docker ​

bash
docker run --rm --cap-add=SYS_ADMIN ubuntu bash
docker run --rm --cap-add=NET_ADMIN ubuntu bash

docker run --rm --cap-drop=SYS_ADMIN ubuntu bash

docker run --rm \
  --cap-add=SYS_ADMIN \
  --cap-add=NET_ADMIN \
  --cap-drop=ALL \
  ubuntu bash

docker run --rm --privileged ubuntu bash

12.6 Configure in runc config.json ​

json
{
  "linux": {
    "capabilities": {
      "bounding": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
      "effective": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
      "permitted": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
      "inheritable": ["CAP_NET_BIND_SERVICE"],
      "ambient": ["CAP_NET_BIND_SERVICE"]
    }
  }
}

12.7 Configure in Kubernetes ​

yaml
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mycontainer
    image: ubuntu
    securityContext:
      capabilities:
        add:
        - SYS_ADMIN
        - NET_ADMIN
        drop:
        - ALL

12.8 Summary: Capability Configuration Methods ​

Permanent (file-level):
  setcap cap_xxx+ep /path/to/binary
          ↓
  File has extended attributes (xattr)
          ↓
  exec() loads into process automatically

Temporary (process-level):
  capsh / cap_set_proc()
          ↓
  Affects only current process and children
          ↓
  Gone when process exits

Container-level:
  docker run --cap-add=xxx
          ↓
  Rewrites container's CapBnd/CapPrm
          ↓
  runc applies via libcontainer

XIII. Summary ​

AttributeMeaningScopeRecoverable
CapEffCurrently active capabilitiesProcess-levelYes, via capset() anytime
CapPrmAllowed capabilities (upper bound)Process-levelYes, via capset() anytime
CapBndGlobal ceiling (Bounding)System-levelNo
CapInhInherited on forkProcess-levelYes, modifiable anytime
CapAmbPreserved after execProcess-levelYes, via capset() anytime

Core design philosophy: Principle of Least Privilege. CapEff should always remain minimal — used and dropped immediately. CapBnd is the last line of defense against privilege escalation.


This is the sixth article in the runc source code analysis series, focusing on Linux Capabilities design philosophy and experimental verification.

Linux Capabilities 详解:进程权限控制的艺术
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2