> 原文连接:Linux Capabilities 入门教程:概念篇html
Linux 是一种安全的操做系统,它把全部的系统权限都赋予了一个单一的 root 用户,只给普通用户保留有限的权限。root 用户拥有超级管理员权限,能够安装软件、容许某些服务、管理用户等。linux
做为普通用户,若是想执行某些只有管理员才有权限的操做,之前只有两种办法:一是经过 sudo
提高权限,若是用户不少,配置管理和权限控制会很麻烦;二是经过 SUID(Set User ID on execution)来实现,它可让普通用户容许一个 owner
为 root 的可执行文件时具备 root 的权限。nginx
SUID
的概念比较晦涩难懂,举个例子就明白了,以经常使用的 passwd
命令为例,修改用户密码是须要 root 权限的,但普通用户却能够经过这个命令来修改密码,这就是由于 /bin/passwd
被设置了 SUID
标识,因此普通用户执行 passwd 命令时,进程的 owner 就是 passwd 的全部者,也就是 root 用户。git
SUID
虽然能够解决问题,但却带来了安全隐患。当运行设置了 SUID
的命令时,一般只是须要很小一部分的特权,可是 SUID
给了它 root 具备的所有权限。这些可执行文件是黑客的主要目标,若是他们发现了其中的漏洞,就很容易利用它来进行安全攻击。简而言之,SUID
机制增大了系统的安全攻击面。github
为了对 root 权限进行更细粒度的控制,实现按需受权,Linux 引入了另外一种机制叫 capabilities
。docker
Capabilities
机制是在 Linux 内核 2.2
以后引入的,原理很简单,就是将以前与超级用户 root(UID=0)关联的特权细分为不一样的功能组,Capabilites 做为线程(Linux 并不真正区分进程和线程)的属性存在,每一个功能组均可以独立启用和禁用。其本质上就是将内核调用分门别类,具备类似功能的内核调用被分到同一组中。shell
这样一来,权限检查的过程就变成了:在执行特权操做时,若是线程的有效身份不是 root,就去检查其是否具备该特权操做所对应的 capabilities,并以此为依据,决定是否能够执行特权操做。安全
Capabilities 能够在进程执行时赋予,也能够直接从父进程继承。因此理论上若是给 nginx 可执行文件赋予了 CAP_NET_BIND_SERVICE
capabilities,那么它就能以普通用户运行并监听在 80 端口上。bash
capability 名称 | 描述 |
---|---|
CAP_AUDIT_CONTROL | 启用和禁用内核审计;改变审计过滤规则;检索审计状态和过滤规则 |
CAP_AUDIT_READ | 容许经过 multicast netlink 套接字读取审计日志 |
CAP_AUDIT_WRITE | 将记录写入内核审计日志 |
CAP_BLOCK_SUSPEND | 使用能够阻止系统挂起的特性 |
CAP_CHOWN | 修改文件全部者的权限 |
CAP_DAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
CAP_DAC_READ_SEARCH | 忽略文件读及目录搜索的 DAC 访问限制 |
CAP_FOWNER | 忽略文件属主 ID 必须和进程用户 ID 相匹配的限制 |
CAP_FSETID | 容许设置文件的 setuid 位 |
CAP_IPC_LOCK | 容许锁定共享内存片断 |
CAP_IPC_OWNER | 忽略 IPC 全部权检查 |
CAP_KILL | 容许对不属于本身的进程发送信号 |
CAP_LEASE | 容许修改文件锁的 FL_LEASE 标志 |
CAP_LINUX_IMMUTABLE | 容许修改文件的 IMMUTABLE 和 APPEND 属性标志 |
CAP_MAC_ADMIN | 容许 MAC 配置或状态更改 |
CAP_MAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
CAP_MKNOD | 容许使用 mknod() 系统调用 |
CAP_NET_ADMIN | 容许执行网络管理任务 |
CAP_NET_BIND_SERVICE | 容许绑定到小于 1024 的端口 |
CAP_NET_BROADCAST | 容许网络广播和多播访问 |
CAP_NET_RAW | 容许使用原始套接字 |
CAP_SETGID | 容许改变进程的 GID |
CAP_SETFCAP | 容许为文件设置任意的 capabilities |
CAP_SETPCAP | 参考 capabilities man page |
CAP_SETUID | 容许改变进程的 UID |
CAP_SYS_ADMIN | 容许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等 |
CAP_SYS_BOOT | 容许从新启动系统 |
CAP_SYS_CHROOT | 容许使用 chroot() 系统调用 |
CAP_SYS_MODULE | 容许插入和删除内核模块 |
CAP_SYS_NICE | 容许提高优先级及设置其余进程的优先级 |
CAP_SYS_PACCT | 容许执行进程的 BSD 式审计 |
CAP_SYS_PTRACE | 容许跟踪任何进程 |
CAP_SYS_RAWIO | 容许直接访问 /devport、/dev/mem、/dev/kmem 及原始块设备 |
CAP_SYS_RESOURCE | 忽略资源限制 |
CAP_SYS_TIME | 容许改变系统时钟 |
CAP_SYS_TTY_CONFIG | 容许配置 TTY 设备 |
CAP_SYSLOG | 容许使用 syslog() 系统调用 |
CAP_WAKE_ALARM | 容许触发一些能唤醒系统的东西(好比 CLOCK_BOOTTIME_ALARM 计时器) |
Linux capabilities 分为进程 capabilities 和文件 capabilities。对于进程来讲,capabilities 是细分到线程的,即每一个线程能够有本身的capabilities。对于文件来讲,capabilities 保存在文件的扩展属性中。微信
下面分别介绍线程(进程)的 capabilities 和文件的 capabilities。
每个线程,具备 5 个 capabilities 集合,每个集合使用 64
位掩码来表示,显示为 16
进制格式。这 5 个 capabilities 集合分别是:
每一个集合中都包含零个或多个 capabilities。这5个集合的具体含义以下:
定义了线程可以使用的 capabilities 的上限。它并不使能线程的 capabilities,而是做为一个规定。也就是说,线程能够经过系统调用 capset()
来从 Effective
或 Inheritable
集合中添加或删除 capability,前提是添加或删除的 capability 必须包含在 Permitted
集合中(其中 Bounding 集合也会有影响,具体参考下文)。 若是某个线程想向 Inheritable
集合中添加或删除 capability,首先它的 Effective
集合中得包含 CAP_SETPCAP
这个 capabiliy。
内核检查线程是否能够进行特权操做时,检查的对象即是 Effective
集合。如以前所说,Permitted
集合定义了上限,线程能够删除 Effective 集合中的某 capability,随后在须要时,再从 Permitted 集合中恢复该 capability,以此达到临时禁用 capability 的功能。
当执行exec()
系统调用时,可以被新的可执行文件继承的 capabilities,被包含在 Inheritable
集合中。这里须要说明一下,包含在该集合中的 capabilities 并不会自动继承给新的可执行文件,即不会添加到新线程的 Effective
集合中,它只会影响新线程的 Permitted
集合。
Bounding
集合是 Inheritable
集合的超集,若是某个 capability 不在 Bounding
集合中,即便它在 Permitted
集合中,该线程也不能将该 capability 添加到它的 Inheritable
集合中。
Bounding 集合的 capabilities 在执行 fork()
系统调用时会传递给子进程的 Bounding 集合,而且在执行 execve
系统调用后保持不变。
Inherited
集合包含该 capability,将继续保留。但若是后续从 Inheritable
集合中删除了该 capability,便不能再添加回来。Linux 4.3
内核新增了一个 capabilities 集合叫 Ambient
,用来弥补 Inheritable
的不足。Ambient
具备以下特性:
Permitted
和 Inheritable
未设置的 capabilities,Ambient
也不能设置。Permitted
和 Inheritable
关闭某权限(好比 CAP_SYS_BOOT
)后,Ambient
也随之关闭对应权限。这样就确保了下降权限后子进程也会下降权限。Permitted
集合中有一个 capability,那么能够添加到 Ambient
集合中,这样它的子进程即可以在 Ambient
、Permitted
和 Effective
集合中获取这个 capability。如今不知道为何也不要紧,后面会经过具体的公式来告诉你。Ambient
的好处显而易见,举个例子,若是你将 CAP_NET_ADMIN
添加到当前进程的 Ambient
集合中,它即可以经过 fork()
和 execve()
调用 shell 脚原本执行网络管理任务,由于 CAP_NET_ADMIN
会自动继承下去。
文件的 capabilities 被保存在文件的扩展属性中。若是想修改这些属性,须要具备 CAP_SETFCAP
的 capability。文件与线程的 capabilities 共同决定了经过 execve()
运行该文件后的线程的 capabilities。
文件的 capabilities 功能,须要文件系统的支持。若是文件系统使用了 nouuid
选项进行挂载,那么文件的 capabilities 将会被忽略。
相似于线程的 capabilities,文件的 capabilities 包含了 3 个集合:
这3个集合的具体含义以下:
这个集合中包含的 capabilities,在文件被执行时,会与线程的 Bounding 集合计算交集,而后添加到线程的 Permitted
集合中。
这个集合与线程的 Inheritable
集合的交集,会被添加到执行完 execve()
后的线程的 Permitted
集合中。
这不是一个集合,仅仅是一个标志位。若是设置开启,那么在执行完 execve()
后,线程 Permitted
集合中的 capabilities 会自动添加到它的 Effective
集合中。对于一些旧的可执行文件,因为其不会调用 capabilities 相关函数设置自身的 Effective
集合,因此能够将可执行文件的 Effective bit 开启,从而能够将 Permitted
集合中的 capabilities 自动添加到 Effective
集合中。
详情请参考 Linux capabilities 的 man page。
上面介绍了线程和文件的 capabilities,大家可能会以为有些抽象难懂。下面经过具体的计算公式,来讲明执行 execve()
后 capabilities 是如何被肯定的。
咱们用 P
表明执行 execve()
前线程的 capabilities,P'
表明执行 execve()
后线程的 capabilities,F
表明可执行文件的 capabilities。那么:
> P'(ambient) = (file is privileged) ? 0 : P(ambient) > > P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding))) | P'(ambient) > > P'(effective) = F(effective) ? P'(permitted) : P'(ambient) > > P'(inheritable) = P(inheritable) [i.e., unchanged] > > P'(bounding) = P(bounding) [i.e., unchanged]
咱们一条一条来解释:
若是用户是 root 用户,那么执行 execve()
后线程的 Ambient
集合是空集;若是是普通用户,那么执行 execve()
后线程的 Ambient
集合将会继承执行 execve()
前线程的 Ambient
集合。
执行 execve()
前线程的 Inheritable
集合与可执行文件的 Inheritable
集合取交集,会被添加到执行 execve()
后线程的 Permitted
集合;可执行文件的 capability bounding 集合与可执行文件的 Permitted
集合取交集,也会被添加到执行 execve()
后线程的 Permitted
集合;同时执行 execve()
后线程的 Ambient
集合中的 capabilities 会被自动添加到该线程的 Permitted
集合中。
若是可执行文件开启了 Effective 标志位,那么在执行完 execve()
后,线程 Permitted
集合中的 capabilities 会自动添加到它的 Effective
集合中。
执行 execve()
前线程的 Inheritable
集合会继承给执行 execve()
后线程的 Inheritable
集合。
这里有几点须要着重强调:
上面的公式是针对系统调用 execve()
的,若是是 fork()
,那么子线程的 capabilities 信息彻底复制父进程的 capabilities 信息。
可执行文件的 Inheritable
集合与线程的 Inheritable
集合并无什么关系,可执行文件 Inheritable
集合中的 capabilities 不会被添加到执行 execve()
后线程的 Inheritable
集合中。若是想让新线程的 Inheritable
集合包含某个 capability,只能经过 capset()
将该 capability 添加到当前线程的 Inheritable
集合中(由于 P'(inheritable) = P(inheritable))。
若是想让当前线程 Inheritable
集合中的 capabilities 传递给新的可执行文件,该文件的 Inheritable
集合中也必须包含这些 capabilities(由于 P'(permitted) = (P(inheritable) & F(inheritable))|...)。
将当前线程的 capabilities 传递给新的可执行文件时,仅仅只是传递给新线程的 Permitted
集合。若是想让其生效,新线程必须经过 capset()
将 capabilities 添加到 Effective
集合中。或者开启新的可执行文件的 Effective 标志位(由于 P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。
在没有 Ambient
集合以前,若是某个脚本不能调用 capset()
,但想让脚本中的线程都能得到该脚本的 Permitted
集合中的 capabilities,只能将 Permitted
集合中的 capabilities 添加到 Inheritable
集合中(P'(permitted) = P(inheritable) & F(inheritable)|...),同时开启 Effective 标志位(P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。有 有 Ambient
集合以后,事情就变得简单多了,后续的文章会详细解释。
若是某个 UID 非零(普通用户)的线程执行了 execve()
,那么 Permitted
和 Effective
集合中的 capabilities 都会被清空。
从 root 用户切换到普通用户,那么 Permitted
和 Effective
集合中的 capabilities 都会被清空,除非设置了 SECBIT_KEEP_CAPS 或者更宽泛的 SECBIT_NO_SETUID_FIXUP。
关于上述计算公式的逻辑流程图以下所示(不包括 Ambient
集合):
下面咱们用一个例子来演示上述公式的计算逻辑,以 ping
文件为例。若是咱们将 CAP_NET_RAW
capability添加到 ping 文件的 Permitted
集合中(F(Permitted)),它就会添加到执行后的线程的 Permitted
集合中(P'(Permitted))。因为 ping 文件具备 capabilities 意识,即可以调用 capset()
和 capget()
,它在运行时会调用 capset()
将 CAP_NET_RAW
capability 添加到线程的 Effective
集合中。
换句话说,若是可执行文件不具备 capabilities 意识,咱们就必需要开启 Effective 标志位(F(Effective)),这样就会将该 capability 自动添加到线程的 Effective
集合中。具备capabilities 意识的可执行文件更安全,由于它会限制线程使用该 capability 的时间。
咱们也能够将 capabilities 添加到文件的 Inheritable
集合中,文件的 Inheritable
集合会与当前线程的 Inheritable
集合取交集,而后添加到新线程的 Permitted
集合中。这样就能够控制可执行文件的运行环境。
看起来颇有道理,但有一个问题:若是可执行文件的有效用户是普通用户,且没有 Inheritable
集合,即 F(inheritable) = 0
,那么 P(inheritable)
将会被忽略(P(inheritable) & F(inheritable))。因为绝大多数可执行文件都是这种状况,所以 Inheritable
集合的可用性受到了限制。咱们没法让脚本中的线程自动继承该脚本文件中的 capabilities,除非让脚本具备 capabilities 意识。
要想改变这种情况,可使用 Ambient
集合。Ambient
集合会自动从父线程中继承,同时会自动添加到当前线程的 Permitted
集合中。举个例子,在一个 Bash 环境中(例如某个正在执行的脚本),该环境所在的线程的 Ambient
集合中包含 CAP_NET_RAW
capability,那么在该环境中执行 ping 文件能够正常工做,即便该文件是普通文件(没有任何 capabilities,也没有设置 SUID)。
最后拿 docker 举例,若是你使用普通用户来启动官方的 nginx 容器,会出现如下错误:
bind() to 0.0.0.0:80 failed (13: Permission denied)
由于 nginx 进程的 Effective
集合中不包含 CAP_NET_BIND_SERVICE
capability,且不具备 capabilities 意识(普通用户),因此启动失败。要想启动成功,至少须要将该 capability 添加到 nginx 文件的 Inheritable
集合中,同时开启 Effective 标志位,而且在 Kubernetes Pod 的部署清单中的 securityContext --> capabilities 字段下面添加 NET_BIND_SERVICE
(这个 capability 会被添加到 nginx 进程的 Bounding
集合中),最后还要将 capability 添加到 nginx 文件的 Permitted
集合中。如此一来就大功告成了,参考公式:P'(permitted) = ...|(F(permitted) & P(bounding)))|...
和 P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
。
若是容器开启了 securityContext/allowPrivilegeEscalation
,上述设置仍然能够生效。若是 nginx 文件具备 capabilities 意识,那么只须要将 CAP_NET_BIND_SERVICE
capability 添加到它的 Inheritable
集合中就能够正常工做了。
固然了,除了上述使用文件扩展属性的方法外,还可使用 Ambient
集合来让非 root 容器进程正常工做,但 Kubernetes 目前还不支持这个属性,具体参考 Kubernetes 项目的 issue。
虽然 Kubernetes 官方不支持,但咱们能够本身来实现,具体实现方式能够关注我后续的文章。
扫一扫下面的二维码关注微信公众号,在公众号中回复◉加群◉便可加入咱们的云原生交流群,和孙宏亮、张馆长、阳明等大佬一块儿探讨云原生技术