传统Linux权限模型下,一个进程要么以普通用户身份运行,要么以UID 0的root身份运行。root拥有全部特权,一旦某个服务被攻破,攻击者就拿到整个系统控制权。Linux capabilities从内核2.2开始引入,把超级用户特权拆分成多个独立能力位,比如CAP_NET_BIND_SERVICE允许绑定1024以下端口,CAP_SYS_TIME允许修改系统时钟。进程可以只拥有其中几个能力,而不再需要完整的root身份。这种权限分割让服务可以降权运行,即使被利用,攻击面也大幅缩小。

能力集合通过线程的cred结构体管理,每个进程都有Effective、Permitted、Inheritable、Bounding和Ambient等集合。理解这些集合是正确使用capabilities的关键,尤其是文件能力与进程能力的继承计算,它决定了为什么有时明明用setcap赋权,程序却仍然没有获得预期能力。
从UID 0到能力位:权限拆分的底层逻辑
在传统的Unix权限检查中,内核大量使用类似if (current->euid == 0)的判断来决定是否放行敏感操作,例如加载内核模块、修改路由表、绑定特权端口等。这种二元模型的问题在于root被赋予了所有特权,任何需要其中一个特权的服务都必须以root运行,无法按需授权。Linux capabilities将root的全部特权拆分成多个能力位,目前内核定义了40多个能力,每个能力对应一类操作。比如CAP_NET_RAW允许使用原始套接字,CAP_SYS_MODULE允许加载和卸载内核模块,CAP_DAC_OVERRIDE允许绕过文件读写和执行权限检查。
能力位在进程的cred结构体中以位图形式存储,用户态可以通过/proc文件系统查看。内核检查权限时不再只判断euid是否为0,而是调用capable(CAP_XXX)检查对应能力位是否在当前进程的Effective集合中。值得注意的是,仅拥有某些能力并不意味着进程可以完全绕过所有安全检查,例如CAP_SYS_ADMIN的范围很广,但仍受制于LSM、seccomp和文件系统挂载选项等机制。把权限拆分后,系统管理员可以更细粒度地决定一个进程到底能做什么。
# 查看当前shell进程的能力掩码 grep Cap /proc/$$/status # 解码能力掩码,capsh通常来自libcap2-bin包 capsh --decode=0000000000000000
上面命令输出中的CapEff表示Effective集合,CapPrm表示Permitted集合,CapBnd表示Bounding集合。如果Eff和Prm的值不是0,说明当前进程具备某些能力。普通shell通常已经不具备任何能力,而root shell的Effective集合几乎包含全部能力位。
文件能力与继承规则:为什么setcap赋值后有时无效
除了进程自身的能力集合,Linux还支持文件能力,通过文件的security.capability扩展属性保存。管理员可以用setcap命令给可执行文件附加能力,这样普通用户运行该文件时,内核会在execve阶段根据文件能力和进程当前能力计算新进程的能力集合,从而在不需要root身份的情况下获得特定能力。例如给自定义的ping程序赋予CAP_NET_RAW,普通用户就能创建原始套接字发送ICMP包。
文件能力的继承规则比想象中复杂。新进程的Permitted集合由进程的Inheritable集合与文件的Inheritable集合做与运算,再与文件Permitted集合和Bounding集合的交集做或运算得到。Effective集合通常由新进程的Permitted集合和文件的Effective位决定,Inheritable集合保持不变。这套规则导致早期文件能力很难跨execve传递,很多管理员遇到setcap后程序仍然没有权限的情况,是因为父进程的Inheritable集合或Bounding集合被裁剪,或者文件的Effective位没有设置。后来内核引入Ambient集合,用于在非特权程序之间保留能力,解决了继承断层问题。
# 给myping文件添加cap_net_raw的Permitted和Effective能力 sudo setcap cap_net_raw+ep /usr/local/bin/myping # 查看文件已附加的能力 getcap /usr/local/bin/myping # 移除文件上的所有能力 sudo setcap -r /usr/local/bin/myping
使用setcap时,+ep表示同时加入Permitted和Effective集合,去掉e可以让程序启动后通过cap_set_proc自行提升能力。需要注意,文件能力对脚本文件无效,因为脚本的解释器是另一个可执行文件,内核只检查解释器本身的能力。对Python或Shell脚本,更好的做法是给解释器或封装二进制赋权,或者使用Ambient能力配合execve。
容器运行时中的capabilities裁剪实践
容器技术在默认情况下已经利用capabilities做了一定的权限收敛。Docker默认只为容器保留一组能力,例如CAP_CHOWN、CAP_DAC_OVERRIDE、CAP_NET_BIND_SERVICE等,但并没有丢弃全部能力。攻击者如果在容器内利用漏洞,仍然可能使用CAP_SYS_ADMIN或CAP_NET_RAW完成逃逸前的准备。因此生产环境通常建议显式裁剪,将不需要的能力全部删除,只添加服务真正依赖的少数能力。
以Docker为例,可以用--cap-drop ALL --cap-add NET_BIND_SERVICE来启动一个只允许绑定低端口的容器。如果服务只需要监听80端口,而不需要修改网络栈或加载模块,那么这个容器失去CAP_SYS_ADMIN、CAP_NET_RAW等能力后,攻击面会明显缩小。Kubernetes中的配置类似,在securityContext里声明drop ALL,再按需add能力。
# Kubernetes Pod安全上下文示例
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
此外,还可以结合no_new_privs参数防止进程获得新权限,使用seccomp过滤不常用的系统调用,配合只读根文件系统和非root用户运行。这些措施叠加起来,即使应用存在远程代码执行漏洞,攻击者也无法轻松提升权限或突破容器隔离。能力裁剪不是一劳永逸的方案,但它是最小权限原则在Linux进程和容器场景中的核心落地手段。

