avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

Linux Capabilities 详解:进程权限控制的艺术

前言 ​

在 上一篇文章 中,我们从 runc 的 SPEC.md 了解了容器的纵深防御体系,留了一个核心问题没有展开:CapPrm 和 CapEff 既然都是 capability 集合,为什么必须分开?CapBnd 和它们有什么区别?

要真正理解这些问题,必须先搞清楚两个前置概念:fork 和 exec。本文从零开始,用实验驱动的方式,把 Capabilities 的五大属性讲透。

一、先修概念:fork 和 exec ​

1.1 fork = 复制一个进程 ​

fork() 是 Linux 系统调用的名字,意思是"复制当前进程":

c
int pid = fork();

if (pid == 0) {
    // 我是子进程
    printf("子进程: PID=%d\n", getpid());
} else {
    // 我是父进程
    printf("父进程: 子进程PID=%d\n", pid);
}

执行结果:

父进程: 子进程PID=101
子进程: 子进程: PID=101

注意:fork 之后,父进程和子进程完全独立,各自运行,互不干扰。

1.2 exec = 换灵魂 ​

execve() 是另一个系统调用,意思是"用新程序替换当前进程的内存空间":

c
// 当前是 bash 进程
execve("/bin/ls", argv, envp);
// 这行之后,当前进程还是 PID=100,但已经变成 ls 了

关键点:进程 PID 不变,运行什么程序变了。

1.3 fork + exec = 跑一个新程序 ​

这是 Linux 最经典的进程创建组合:

shell (PID=100)
  fork() → 子进程 (PID=101,是 shell 的克隆)
    execve("/bin/runc", ...) → PID=101 的进程变成了 runc
      runc 开始运行

Docker 容器创建进程的方式本质上就是这样:先 fork 再 exec。

二、Capabilities 的五大属性 ​

Linux 中的 process capabilities 由五个集合组成,可以用 cat /proc/self/status 查看:

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

2.1 CapEff(Effective)—— 当前生效的能力 ​

进程当前实际使用的 capabilities。

类比:你手里正在用的工具。

2.2 CapPrm(Permitted)—— 已允许的能力 ​

进程最多能拥有的 capabilities 的上限。

类比:你包里装的工具。手里可以不用,但包里得有。

2.3 CapBnd(Bounding)—— 全局上限 ​

系统级限制,规定了一个进程这辈子最多能拥有哪些 capabilities。

类比:你的最高学历。可以低,不能高;丢了就回不来。

2.4 CapInh(Inheritable)—— fork 时继承 ​

fork 产生的子进程能从父进程继承哪些 capabilities。

类比:遗传基因,子进程自动获得。

2.5 CapAmb(Ambient)—— exec 后保留 ​

exec(程序替换)后仍能保留的 capabilities(仅对非 root 程序有效)。

类比:随身携带的证件,exec 后依然有效。

三、CapPrm 和 CapEff 为什么必须分开? ​

这是 Capabilities 设计最精妙的地方——最小权限原则(Principle of Least Privilege)。

3.1 场景一:Web 服务器 ​

nginx 进程需要绑定 80 端口(cap_net_bind_service),但它 99% 的时间在处理普通 HTTP 请求,完全不需要这个能力。

启动时:
  CapPrm = cap_net_bind_service, cap_setuid, cap_sys_admin ...
  CapEff = cap_net_bind_service  ← 绑定 80 端口时短暂需要

bind() 完成后:
  CapEff = 空  ← 用完了就丢

处理 10000 个 HTTP 请求:
  CapEff = 空  ← 正常工作不需要任何特权

遇到 CGI 脚本需要执行:
  CapEff = cap_setuid  ← 临时开启
  执行完成
  CapEff = 空  ← 再丢

... 如此循环 ...

好处:如果黑客攻破 nginx 进程,只能拿到 CapEff 那一刻生效的能力。如果 CapEff 全程等于 CapPrm = 全开,黑客立刻拥有所有 capability。

3.2 场景二:setuid 程序降权 ​

sudo 程序的运行过程:

如果 CapEff 和 CapPrm 绑在一起,降权后想再临时提权就非常麻烦。

3.3 场景三:fork 子进程继承 ​

父进程:
  CapInh = {cap_net_bind}
  CapPrm = {cap_net_bind}
  CapEff = {cap_net_bind}

fork() 后:
  子进程自动继承:
    CapInh = {cap_net_bind}
    CapPrm = {cap_net_bind}(子进程从父进程继承 CapPrm)
    CapEff = {cap_net_bind}

子进程立刻丢弃 CapEff:
    CapPrm = {cap_net_bind}(还在包里,没丢)
    CapEff = 空

→ 如果子进程被攻击者利用,攻击者拿到的是 CapEff = 空
  而不是 CapPrm = {cap_net_bind}

3.4 一句话总结 ​

CapPrm 是"弹药库",CapEff 是"手里的枪"。弹药库可以被偷,但枪不在手里就开不了火。分开是为了:任何时候都尽量让 CapEff = 空或最小集,被攻击了也只丢最小权限。

四、CapBnd 和 CapPrm 的区别 ​

这是最容易混淆的一对概念:

CapBndCapPrm
作用域系统级,整个进程树的全局限制进程级,单个进程自己的限制
能不能恢复不能,丢弃就是丢了,exec 后也回不来能,capset() 可以重新开启
初始值内核 cap_last_cap 决定(系统所有 capability 的全集)来自父进程 fork/exec 时的继承

4.1 形象比喻 ​

CapBnd = 你的"最高学历"(博士)
         → 你这辈子最多拿到博士学位,不可能再高了
         → 即使丢了,以后也不可能再拿回来(除非重启)

CapPrm = 你包里的"证件"(工程师证 + 教师证 + ...)
         → 可以随时丢弃,也可以随时考新的补回来

4.2 CapBnd 的真正价值:阻止提权 ​

CapBnd 是防止已经拥有 root 权限的进程提权的最后防线。

Docker 容器逃逸场景:

宿主机 runc 进程:
  CapBnd = 全开(40 个 capability 都有)
  CapPrm = 全开
  CapEff = 全开

容器创建时,runc 会裁剪容器的 CapBnd(去掉 cap_sys_admin 等):
  容器内进程:
    CapBnd = {去掉 CAP_SYS_ADMIN}     ← 内核强制限制!
    CapPrm = {去掉 CAP_SYS_ADMIN}
    CapEff = {去掉 CAP_SYS_ADMIN}

  → 即使容器内进程通过漏洞提权成功,也无法执行 mount()/unshare()/chroot()
    因为这些操作需要 CAP_SYS_ADMIN,而 CapBnd 里没有

CapBnd 是铁门,CapPrm 是木门。

4.3 操作对比 ​

CapBnd 只能丢弃,不能恢复:

bash
capsh --drop=cap_sys_admin --

capsh --add=cap_sys_admin --print

CapPrm 可以丢弃和恢复:

bash
capsh --drop=cap_net_bind_service --

capsh --add=cap_net_bind_service --

五、CapInh 和 CapAmb 的区别 ​

CapInhCapAmb
生效时机fork 时继承exec 时保留
用途控制子进程能拿到哪些 capabilities控制 exec 后还能不能用
常见场景需要 fork 出带权限的子进程需要 exec 后保持某些权限

5.1 CapInh 的工作原理 ​

父进程:
  CapInh = {cap_net_bind_service}
  fork()
    ↓ 子进程继承
  子进程 CapInh = {cap_net_bind_service}(自动有)
  子进程 CapPrm = {cap_net_bind_service}(从 CapInh 继承)

5.2 CapAmb 的工作原理 ​

普通程序 exec 后会丢失所有 capabilities:

普通进程(无 root):
  execve("/bin/ls", ...)
    → 丢失所有 capabilities(因为 ls 不是 setuid 程序)

CapAmb 允许 exec 后保留一部分:

普通进程:
  CapPrm = {cap_net_bind_service}
  CapEff = {cap_net_bind_service}
  CapAmb = {cap_net_bind_service}  ← 标记了"exec 后保留"

  execve("/bin/myprogram", ...)
    → myprogram 仍然有 cap_net_bind_service

5.3 CapAmb 的三个限制 ​

  1. 程序不能是 setuid/setgid 程序
  2. CapAmb ≤ CapPrm(不能保留没在包里有的能力)
  3. CapAmb ≤ CapInh(不能保留没被标记继承的能力)

六、CapInh 和 CapAmb 的实际案例 ​

6.1 CapInh 案例:Nginx fork worker 进程 ​

nginx master 进程(root):
  CapInh = {cap_net_bind_service}  ← 设置了需要继承的能力
  fork()
    ↓ 子进程继承
  nginx worker 进程:
    CapPrm = {cap_net_bind_service}(从 CapInh 继承)
    CapEff = {cap_net_bind_service}

6.2 CapAmb 案例:普通用户运行网络服务 ​

普通用户进程:
  CapAmb = {cap_net_bind_service}
  execve("/usr/sbin/my-server", ...)
    → my-server 仍然可以绑定 80 端口(即使不是 root)

七、完整初始化过程 ​

进程启动(exec):
  CapBnd = 内核 cap_last_cap 决定的最大集合(通常 40 个全开)
         ↓
  CapPrm = 继承自父进程
         ↓
  CapEff = 继承自父进程
         ↓
  CapInh  = 继承自父进程(可被程序显式修改)
         ↓
  CapAmb  = 继承自父进程(可被程序显式修改)

八、查看和操作 Capabilities 的常用命令 ​

8.1 查看当前进程的 capabilities ​

bash
cat /proc/self/status | grep Cap

输出示例:

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

8.2 解码十六进制为可读列表 ​

bash
sudo apt install libcap-ng-utils

capsh --decode=000001ffffffffff

输出示例:

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 工具详解 ​

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 查看任意进程的 capabilities ​

bash
cat /proc/1234/status | grep Cap

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

8.5 getpcaps 工具(查看其他进程) ​

bash
sudo apt install libcap-ng-utils

getpcaps 1234

九、实验演示 ​

9.1 实验一:观察 root 和普通用户的 capabilities 差异 ​

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

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

对比:root 的 CapEff 通常是全开(000001ffffffffff),普通用户的 CapEff 通常只有部分。

9.2 实验二:CapBnd 丢弃后无法恢复 ​

bash
capsh --drop=cap_sys_admin --

capsh --add=cap_sys_admin --print

9.3 实验三:fork 后 CapInh 继承 ​

在有 root 权限的环境中:

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

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

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

9.4 实验四:exec 后 CapAmb 保留 ​

普通用户(无 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 实验五:观察 Docker 容器的 CapBnd ​

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

cat /proc/self/status | grep Cap

容器的 CapBnd 通常比宿主机少很多(比如没有 CAP_SYS_ADMIN)。

十、runc 中 Capabilities 的应用 ​

在 runc 创建容器时,会根据 config.json 中的 linux.capabilities 字段设置容器进程的 capabilities:

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

    // 2. 只开启 config.json 中声明的 capabilities
    for _, cap := range c.bounds {
        cap.Set(cap.Permitted, true)
        cap.Set(cap.Effective, true)
    }

    return nil
}

容器的 CapBnd 也会被裁剪,去掉容器不需要的高危 capabilities(如 CAP_SYS_ADMIN、CAP_SYS_MODULE 等),防止容器内进程通过这些 capability 实现逃逸。

十一、Seccomp 详解:Capabilities 的补充 ​

11.1 什么是 Seccomp ​

Seccomp(Secure Computing Mode)是 Linux 内核的系统调用过滤器,用来限制进程能调用哪些系统调用(syscall)。

每个程序都通过系统调用和内核交互:

用户程序 ──read()──→ 内核
用户程序 ──write()──→ 内核
用户程序 ──mount()──→ 内核  ← 高危!
用户程序 ──unshare()──→ 内核  ← 高危!

Seccomp 的作用是在 syscall 到达内核处理函数之前就拦截它:

没有 Seccomp:
  程序 ──mount──→ ✅ 通过  ← 危险!

有 Seccomp(只允许 read/write/exit):
  程序 ──mount──→ ❌ 拒绝(返回 -1,errno = EPERM)

11.2 Seccomp 的两种模式 ​

Mode 1(严格模式,内核 2.6.12 引入)

只能完全允许或完全禁止,不灵活:

bash
echo 1 > /proc/self/seccomp

Mode 2(过滤模式,内核 3.5 引入)

可以基于规则过滤 syscall,主流用法:

允许:read, write, exit, sigreturn
禁止:mount, unshare, mknod, chroot, ...

11.3 Capabilities 和 Seccomp 的关系 ​

这两个确实有功能交叉,但设计目的不同:

CapabilitiesSeccomp
控制粒度单个 capability单个系统调用
层次Linux 用户态权限模型内核强制执行
关系"你有没有这个证""你有没有调用这个 syscall 的权利"

关键区别:

Capabilities 检查在内核 syscall 处理函数内部(有证才放行)
Seccomp 过滤在 syscall 到达处理函数之前就拦截了

两者叠加是双重保险:

攻击者通过漏洞获取了 CAP_SYS_ADMIN:

只有 Capabilities:
  mount() ──→ 有 CAP_SYS_ADMIN ──→ 成功 ⚠️ 容器逃逸!

叠加 Seccomp:
  mount() ──→ Seccomp 拦截 ──→ EPERM ❌
              即使有 CAP_SYS_ADMIN 也白搭

形象比喻:

Capabilities = 保安(检查你有没有通行证)
Seccomp      = 门上锁(直接物理上禁止开这扇门)

11.4 查看和配置 Seccomp ​

查看进程的 Seccomp 状态:

bash
cat /proc/self/status | grep Seccomp

启用 Seccomp(Mode 2):

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

// 启用 Seccomp 过滤模式
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

用 libseccomp 编写规则:

c
#include <seccomp.h>

int main() {
    // 创建过滤规则
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
    // 默认允许所有 syscall

    // 添加规则:禁止 mount
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
                     SCMP_SYS(mount), 0);

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

    // 加载规则
    seccomp_load(ctx);

    // 现在 mount() 会被拒绝
    mount("none", "/tmp", "tmpfs", 0, NULL);  // 返回 EPERM
}

验证 Seccomp 拦截:

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

runc 中的 Seccomp 配置(config.json):

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

十二、如何给进程配置 Capability ​

12.1 查看进程的 Capability ​

bash
cat /proc/self/status | grep Cap

getpcaps <pid>

12.2 给可执行文件设置 Capability(永久) ​

使用 setcap 命令,给二进制文件设置:

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 给运行中的进程调整 Capability(临时) ​

使用 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 在代码中设置 Capability(libcap) ​

c
#include <sys/capability.h>

int main() {
    // 获取当前进程的 capability
    cap_t caps = cap_get_proc();

    // 清除所有 capabilities
    cap_clear(caps);

    // 添加 cap_net_bind_service 到 Effective 和 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);

    // 应用到当前进程
    cap_set_proc(caps);

    // 现在可以绑定 80 端口了
    bind(sock, ...);

    cap_free(caps);
    return 0;
}

编译:

bash
gcc -o myapp myapp.c -lcap

12.5 在 Docker 中设置 Capability ​

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 在 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 在 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 一图总结 Capability 配置方式 ​

永久配置(文件级别):
  setcap cap_xxx+ep /path/to/binary
          ↓
  文件有 extended attributes (xattr)
          ↓
  exec() 时内核自动加载到进程

临时配置(进程级别):
  capsh / cap_set_proc()
          ↓
  只影响当前进程和子进程
          ↓
  进程退出就没了

容器内配置:
  docker run --cap-add=xxx
          ↓
  改写容器的 CapBnd/CapPrm
          ↓
  runc 通过 libcontainer 应用到容器进程

十三、总结 ​

属性含义作用域能否恢复
CapEff当前生效的能力进程级随时 capset()
CapPrm已允许的能力(上限)进程级随时 capset()
CapBnd全局上限(Bounding)系统级不能
CapInhfork 时继承进程级随时修改
CapAmbexec 后保留进程级随时 capset()

核心设计思想:最小权限原则。CapEff 应该始终保持最小集合,用完就丢;CapBnd 是防止提权的最后防线。


本文是 runc 源码解读系列的第六篇,重点剖析了 Linux Capabilities 的设计哲学与实验验证。

runc 源码解读(九):OCI Spec 核心数据结构解析 (下) —— 资源控制与系统调用过滤
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2