如果你使用 runc
运行一个容器并执行以下操作,会得到有趣的结果:
|
|
即使我们使用的是 UID
为 0 的 root
用户,也没有权限执行修改 hostname 的操作。
实际上 root
用户拥有最高特权早就成了过去式,Linux 内核在 2.2 版本就引入了一种新的权限检查机制 - capabilities。
比超级用户更细粒度的权限控制
传统的 Linux 权限检查模型较为简单,内核在进行权限检查时只会区分两类进程:
- 特权进程,其有效用户 ID为 0,该用户也就是我们常说的超级用户或
root
。 - 非特权进程,有效用户 ID 不为 0。
特权进程将直接绕过内核的所有检查,非特权进程则需要基于进程的有效用户 ID 和有效用户组 ID 等凭证执行检查。
为了适应更复杂的权限需求,从 2.2 版本起 Linux 内核能够进一步将超级用户的权限分解为细颗粒度的单元,这些单元称为 capabilities。例如,capability CAP_CHOWN
允许用户对文件的 UID 和 GID 进行任意修改,即执行 chown
命令。几乎所有与超级用户相关的特权都被分解成了单独的 capability。
capabilities 的引入有以下好处:
- 从超级用户的权限中移除部分 capability 以削弱其权限,提高系统的安全性。
- 可以根据需求非常精准地向普通用户授予部分特殊权限。
特权容器的安全风险
容器通过 namespace 来隔离进程和资源,但并不是所有的资源都可以被 namespace 化,容器和宿主机并不是完全隔离的,比如容器和宿主机中的时间就是共享的。如果容器中的进程拥有一切特权,它可以运行直接访问硬件的(恶意)程序甚至直接修改宿主机的文件系统,因此有必要对容器中的操作进行一定的限制,否则会影响到宿主机的稳定性,甚至带来严重的安全风险。
出于以上考虑,默认情况下容器运行时使用白名单的方式在创建容器时加入一部分的 capabilities,在容器中即使你是超级用户也没有权限执行特定的操作。
接下来我们通过实例加深对容器中 capabilities 的认识。
准备工作
我们将在容器中使用额外的工具库 libcap
实现和 capabilities 的交互,为此需要将其安装到一个 filesystem bundle
中,这种方式在之前的文章中已有介绍,具体方式如下:
|
|
然后就可以使用 runc run
从 /mycontainer2
目录运行一个已安装该库的基础容器了。
创建容器时添加 capabilities
在开头的示例中,我们无法在容器中以 root
用户设置 hostname,是因为缺少了 CAP_SYS_ADMIN
这一 capability,它并未包含在容器默认添加 capabilities 的白名单中。
在之前的一篇文章中,我们介绍过容器运行时会根据 bundle
中的 config.json
,为其创建的容器设置运行参数和执行环境,这一过程也包括了设置容器内进程的 capabilities。
通过修改 config.json
,向 JSON 中的 process.capabilities
对象的 bounding
,permitted
和 effective
列表中加入 "CAP_SYS_ADMIN"
,该 capability 将加入到容器 init
进程的对应 capabilities 集合中。
|
|
capabilities 的技术细节
capabilities 可以应用于文件和进程(或线程,Linux 内核不区分进程和线程),文件的 capabilities 存储在文件的扩展属性中,扩展属性在构建镜像时会被清理掉,所以在容器中我们基本不需要考虑文件的 capabilities。
进程的 capabilities 通过每个进程单独维护的 5 个 capability 集合来控制,每个集合中都包含 0 个或多个 capabilities:
- Permitted:进程所能够使用的 capabilities 的超集
- Inheritable:进程在执行
exec()
系统调用时,能够被新的派生进程所继承的 capabilities - Effective:内核对进程执行权限检查时所使用的集合
- Bounding:Inheritable 集合的超集,一个capability 必须在 Bounding 集合中才能添加到Inheritable
- Ambient:非特权程序执行
exec()
系统调用时将保留的 capabilities
如上所示我们向 init
进程的 Permitted、Bounding 和 Effective 集合中加入了 CAP_SYS_ADMIN
,因此 init
进程将通过内核对 CAP_SYS_ADMIN
的检查。
下面我们根据新的 config.json
运行一个新的容器,现在可以修改 hostname 了:
|
|
执行以上操作时,我们位于作为容器 init
进程的 sh
进程中,如果在容器中继续创建新的进程,是否也会具有新加入的 capability?我们来试一下,在一个新的窗口中执行以下命令:
|
|
修改 hostname 的操作执行成功,因为新创建的进程完全复制了 init
进程的 capabilities。
容器运行时添加 capabilities
除了修改 config.json
加入 capabilities,我们还能够在容器运行时阶段添加 capabilities。
首先将 config.json
还原,然后运行一个新的容器 mybox3
,在新的 sh
进程中确认已经不再具有 CAP_SYS_ADMIN
。
然后通过 runc exec
在该容器中创建一个新的进程,并通过 --cap
选项为该进程添加 CAP_SYS_ADMIN
:
|
|
该操作的原理是,既然 runc
能够根据 config.json
设置 init
进程的 capabilities 集合,它同样也能为容器内运行的其他进程设置。
查看进程具有的 capabilities
capsh
在容器内执行 capsh --print
能够获取到更多关于 capabilities 的信息:
|
|
该命令打印了当前进程所具有的 capabilities。
在 Current
和 Bounding set
中包含了我们通过 config.json
加入的 cap_sys_admin
。capability 末尾的 +eip
代表了该 capability 同时存在于 Effective,Inheritable 和 Permitted 集合中。
pscap
首先在宿主机获取容器中已运行进程的 PID:
|
|
在宿主机中安装 pscap
程序:
|
|
根据获得的 PID,查看容器中进程所具有的 capabilities:
|
|