[TOC]

在这里插入图片描述

include/linux/security.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief security_capable - 检查凭证是否拥有特定能力(LSM钩子包装)。
* @param cred 待检查的进程凭证。
* @param ns 目标资源所属的用户命名空间。
* @param cap 要检查的能力编号。
* @param opts 附加选项。
* @return 0 表示拥有能力,-EPERM 表示没有。
*/
static inline int security_capable(const struct cred *cred,
struct user_namespace *ns,
int cap,
unsigned int opts)
{
// 这是一个内联包装函数,直接调用底层的能力检查核心函数。
return cap_capable(cred, ns, cap, opts);
}

security/commoncap.c Linux权能(Linux Capabilities) 打破root特权的细粒度权限控制

历史与背景

这项技术是为了解决什么特定问题而诞生的?

Linux权能(Capabilities)机制的诞生是为了解决传统UNIX系统中一个根本性的安全设计缺陷:“全有或全无”的root特权模型。 在此模型下,进程分为两类:特权进程(有效UID为0,即root)和非特权进程。 特权进程可以绕过内核的所有权限检查,而非特权进程则受到严格的凭证(UID、GID等)限制。 这种二元模型带来了严重的安全风险:一个程序哪怕只需要一项特权操作(例如,Web服务器需要绑定到小于1024的端口),也必须以完整的root权限运行。 一旦该程序存在漏洞,攻击者就能利用它获得整个系统的控制权。

Capabilities技术通过将历史上与超级用户关联的庞大特权分解为一组组独立的、细粒度的权限单元,从根本上解决了这个问题。 这种机制允许系统遵循最小权限原则,即只授予一个进程执行其任务所必需的最小权限集,从而显著减小攻击面。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux Capabilities的发展历程反映了其从一个基础概念到成熟安全框架的演进:

  • 早期引入:Capabilities的概念早在Linux 2.2内核(约1999年)中就被引入,最初只作用于进程。
  • 文件权能:一个重要的里程碑是在2008年(大约在内核2.6.24之后),引入了对文件权能的支持。 这项技术允许将权能附加到可执行文件上,当该文件被执行时,进程可以获得这些特定的权能,这成为替代高风险的setuid-root二进制程序的现代化方案。 文件权能通过扩展属性(extended attribute)security.capability实现。
  • 权能集(Capability Sets)的演进:随着需求的明确,权能被划分为多个集合进行管理,包括Effective(当前生效的)、Permitted(允许拥有的)、Inheritable(可被子进程继承的)等。
  • 权能边界集(Bounding Set):引入了边界集(Bounding Set)的概念,作为一个进程及其子进程所能拥有的权能的上限,提供了一个额外的安全约束。
  • 环境权能(Ambient Capabilities):在Linux 4.3中引入了环境权能集,解决了在execve()之后,非setuid程序如何安全地继承和保持权能的问题,这对于容器和脚本化环境尤为重要。

目前该技术的社区活跃度和主流应用情况如何?

Capabilities是一项非常成熟且被广泛应用的核心内核安全机制。它已成为现代Linux系统安全体系的基石:

  • 容器化技术:Docker、Kubernetes等容器平台严重依赖Capabilities机制来加固容器安全。默认情况下,容器仅被授予一个有限的权能子集,并丢弃了许多高风险权能。
  • 系统服务:现代系统服务(如systemd管理的服务)越来越多地使用Capabilities来限制自身权限,而不是以完整的root身份运行。
  • 网络工具:像ping这样的网络工具,过去依赖setuid-root来创建原始套接字(raw socket),现在则通过赋予CAP_NET_RAW文件权能来实现相同功能,同时安全性更高。

核心原理与设计

它的核心工作原理是什么?

security/commoncap.c 提供了Linux权能检查的核心逻辑。它作为Linux安全模块(LSM)框架的一部分,通过在内核的关键代码路径上注册钩子(hooks)来工作。

  1. 权能检查点:在内核代码中,任何需要特权的操作(如chown(), socket(), settimeofday())在执行前,都会调用一个通用的权限检查函数,如capable()
  2. LSM钩子触发capable()函数会触发LSM框架中的capable钩子。
  3. cap_capable()的执行security/commoncap.c中实现的cap_capable()函数被注册到了这个钩子上。因此,每次权限检查都会最终调用到cap_capable()
  4. 权能集验证cap_capable()函数的核心逻辑是检查当前进程的**有效权能集(Effective Capability Set)**中是否包含了执行该操作所必需的特定权能位。 例如,要绑定到80端口,进程的有效权能集中必须包含CAP_NET_BIND_SERVICE
  5. 裁决:如果检查通过(即进程拥有所需权能),函数返回0,内核继续执行该操作。如果检查失败,函数返回一个错误码(如-EPERM),内核则会拒绝该操作。

commoncap.c不仅实现了这个核心检查逻辑,还包含了管理进程权能集(通过capset()系统调用)和处理文件权能在execve()期间如何传递给新进程的复杂计算逻辑。

它的主要优势体现在哪些方面?

  • 细粒度权限:将root的权力分解为近40个独立的权能,实现了精确的权限控制。
  • 遵循最小权限原则:使得程序可以只被授予其功能所必需的最小权限,极大地降低了潜在漏洞的危害。
  • 替代setuid:提供了一种比setuid二进制程序更安全的替代方案,避免了因setuid程序漏洞导致整个系统被攻陷的风险。
  • 提升容器安全:是实现容器隔离和安全加固的关键技术之一,有效防止容器逃逸。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 复杂性:管理和理解近40个不同的权能以及它们之间的交互,比传统的UID/GID模型要复杂得多。
  • CAP_SYS_ADMIN的过载:历史上,许多新的特权操作都被归入了CAP_SYS_ADMIN权能,使其成为一个“包罗万象”的超级权能,违背了细粒度分离的初衷。尽管后续内核版本在努力拆分它,但这个问题依然存在。
  • 并非所有操作都被转换:内核中仍有一些遗留的特权检查直接判断UID == 0,而没有被转换为特定的权能检查,这意味着在某些情况下,仅有权能而没有root身份仍然无法完成操作。
  • 管理工具的依赖:需要用户空间工具(如setcap, getcap, capsh)来管理和查看权能,需要一定的学习成本。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 网络服务绑定低位端口:一个Web服务器(如Nginx)需要监听80或443端口。传统上需要以root身份启动,然后再降权。使用Capabilities,只需给Nginx可执行文件赋予CAP_NET_BIND_SERVICE权能,它就可以由一个非root用户直接启动并成功绑定端口。
    • 命令示例:sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx
  • 网络数据包捕获:网络分析工具(如tcpdumpwireshark)需要访问网络接口以捕获数据包。这需要创建原始套接字,对应CAP_NET_RAW权能。
    • 命令示例:sudo setcap cap_net_raw+ep /usr/sbin/tcpdump
  • 容器权限管理:在Docker或Kubernetes中,当一个容器需要执行特定特权操作但又不应给予--privileged(完全特权)时,精确地添加所需权能是最佳实践。例如,一个需要修改网络路由表的容器可以被授予CAP_NET_ADMIN
    • Docker命令示例:docker run --cap-drop=ALL --cap-add=NET_ADMIN my_image

是否有不推荐使用该技术的场景?为什么?

  • 需要完整系统管理权限的场景:对于像sshd这样需要进行用户会话管理、修改系统配置等广泛管理任务的程序,试图用一组有限的权能来替代root权限会变得极其复杂甚至不可能。
  • 简单的、无特权要求的应用:对于绝大多数不需要任何特权的普通应用程序(如文本编辑器、计算器),根本无需涉及Capabilities。传统的用户和文件权限模型已经足够。

对比分析

请将其 与 其他相似技术 进行详细对比。

Capabilities与传统的UID模型以及其他LSM(如SELinux/AppArmor)共同构成了Linux的权限控制体系,但它们的侧重点和机制各不相同。

特性 Linux Capabilities 传统UID模型 (root/non-root) SELinux / AppArmor (LSM)
功能概述 将root特权分解为细粒度的、可独立授予的权限单元。 一个二元的“全有或全无”模型,基于用户身份(UID 0 vs 非0)进行权限检查。 提供强制访问控制(MAC),基于安全策略规则(标签或路径)来控制主体(进程)对客体(文件、套接字等)的访问。
控制粒度 中等。基于“操作”或“能力”,如“能否绑定低位端口”。 粗糙。只有“能”或“不能”两种状态。 非常精细。可以控制到具体文件、具体操作类型(读、写、执行、追加等)。
安全目标 最小权限原则,减少进程的潜在攻击面。 用户隔离,区分系统管理员和普通用户。 强制访问控制,即使是root用户也要受到策略的严格限制,防止权限滥用和未知漏洞利用。
实现方式 通过LSM钩子在内核中检查进程的有效权能集。 在内核代码中直接检查进程的有效UID是否为0。 通过LSM钩子,根据预定义的策略规则匹配主体和客体的安全上下文(标签)或路径名。
易用性 中等。概念相对直观,但管理和调试有一定学习曲线。 非常简单。易于理解和使用。 复杂(特别是SELinux)。策略编写和维护非常困难,需要专业知识。
结合关系 互补。Capabilities检查通常发生在LSM检查之前。一个操作需要同时通过Capabilities和SELinux/AppArmor的检查才能被允许。 Capabilities旨在打破传统UID模型的局限性。一个非root用户可以拥有权能,一个root用户也可以被剥夺权能。

cap_capable: 跨用户命名空间的能力检查机制

本代码片段是 Linux 内核安全子系统的基石之一,提供了核心函数 cap_capable,用于精确地判断一个进程(由其凭证 cred 代表)是否拥有某个特定的能力(Capability),并且这个判断过程完全支持并正确处理了复杂的用户命名空间(User Namespace)层次结构。当内核的其他部分需要进行权限检查时(例如,判断一个进程是否可以重启系统),它们最终都会调用这个函数。

实现原理分析

此机制的核心原理是基于用户命名空间(User Namespace)的层次化能力模型。一个进程的能力不再是一个简单的全局属性,而是与其所属的用户命名空间相关联。cap_capable_helper 函数中的 for(;;) 循环通过向上遍历命名空间树,实现了这一复杂的检查逻辑。

  1. 向上遍历(Upward Traversal): 检查的起点是目标资源所属的命名空间 target_ns。循环通过 ns = ns->parent 不断向上移动,直至根命名空间(init_user_ns)。

  2. 能力检查的三个关键规则: 在循环的每一层,代码会依次应用以下规则:

    • 规则一:同命名空间内的直接检查if (likely(ns == cred_ns)):这是最常见和最高效的情况。如果当前检查的命名空间 ns 正是进程凭证所属的命名空间 cred_ns,那么就直接检查该进程的有效能力集(cred->cap_effective)中是否包含所需的能力位。这是通过位掩码操作 cap_raised 实现的。
    • 规则二:命名空间所有者特权if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid)):这是用户命名空间的一个核心特性。一个用户在一个父命名空间中创建了一个新的子命名空间,那么该用户(ns->owner)在这个新的子命名空间内自动获得全部能力。此规则检查的就是这种情况:如果当前检查的命名空间 ns 的父命名空间正好是进程所在的命名空间,并且该进程的有效用户ID(cred->euid)与 ns 的所有者ID 匹配,那么就授予权限。
    • 规则三:父命名空间能力继承。循环本身 ns = ns->parent 体现了继承原则。如果一个进程在某个父命名空间中拥有某项能力,那么它自动对该父命名空间下的所有子孙命名空间都拥有该项能力。循环会持续向上,直到在某个父命名空间中通过了规则一的直接检查。
  3. 提前终止优化: if (ns->level <= cred_ns->level) 检查是一个重要的优化。level 代表命名空间的嵌套深度。如果向上遍历的过程中,ns 的深度已经小于或等于进程凭证 cred_ns 的深度,但 ns 却不等于 cred_ns,这说明它们处于不同的分支, 沿着这条路继续前进是没有意义的。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @brief cap_capable - 判断一个任务是否拥有特定的有效能力。
* @param cred 要使用的凭证。
* @param target_ns 被访问资源所属的用户命名空间。
* @param cap 要检查的能力。
* @param opts 在 include/linux/security.h 中定义的选项位掩码(在此函数中未使用)。
* @return 成功(拥有能力)返回0,失败(没有能力)返回负数错误码。
*
* @note 与 capable() 等函数相反,cap_capable() 在任务拥有能力时返回0,
* 而 capable() 及其变体在这种情况下返回布尔值true(1)。
*/
int cap_capable(const struct cred *cred, struct user_namespace *target_ns,
int cap, unsigned int opts)
{
// 获取凭证自身所属的用户命名空间。
const struct user_namespace *cred_ns = cred->user_ns;
// 调用辅助函数执行实际的检查逻辑。
int ret = cap_capable_helper(cred, target_ns, cred_ns, cap);

// 记录一次能力检查的追踪事件,用于调试和审计。
trace_cap_capable(cred, target_ns, cred_ns, cap, ret);
return ret;
}

/**
* @brief cap_capable_helper - 判断任务是否拥有特定有效能力的辅助函数。
* @param cred 要使用的凭证。
* @param target_ns 被访问资源所属的用户命名空间。
* @param cred_ns 凭证所属的用户命名空间。
* @param cap 要检查的能力。
* @return 成功返回0,失败返回负数错误码。
*/
static inline int cap_capable_helper(const struct cred *cred,
struct user_namespace *target_ns,
const struct user_namespace *cred_ns,
int cap)
{
// 从目标命名空间开始向上遍历。
struct user_namespace *ns = target_ns;

/*
* 通过检查目标用户命名空间及其所有父命名空间,
* 来判断cred是否在目标用户命名空间中拥有该能力。
*/
for (;;) {
// 我们是否拥有必要的能力?
// [规则一] 如果当前检查的命名空间就是凭证自己的命名空间...
if (likely(ns == cred_ns))
// ...那么直接检查凭证的有效能力集中是否包含该能力。
return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;

/*
* 如果我们已经到达了比我们凭证所在层级更低或相同的层级
* (但ns != cred_ns),那么搜索结束,肯定没有权限。
*/
if (ns->level <= cred_ns->level)
return -EPERM;

/*
* [规则二] 用户命名空间的创建者在其父命名空间中,
* 对其创建的子命名空间拥有全部能力。
*/
if ((ns->parent == cred_ns) && uid_eq(ns->owner, cred->euid))
return 0;

/*
* [规则三] 如果你在一个父用户命名空间中拥有某项能力,
* 那么你对它所有的子用户命名空间也拥有该能力。
*/
// 移动到父命名空间,继续下一轮检查。
ns = ns->parent;
}

/* 我们永远不会执行到这里 */
}