在本系列的上一部分中,我们提到 Docker 容器尚未使用 time 命名空间。我们还探讨了容器在许多情况下如何以 root 用户身份运行。考虑到这两点,如果我们尝试更改容器内的日期和时间会发生什么?
为了测试这一点,我们先运行 docker run -it ubuntu:22.04 /bin/bash 然后,我们将尝试使用 date +%T -s "08:08:08" 更改时间。返回的结果是:date: cannot set date: Operation not permitted
root 用户无法更改容器中的时间
鉴于我们是root 用户,通常设置时间肯定是没问题的,因此一定有一些其他功能阻止它工作。
这就是这篇文章的重点:Linux Capabilities。
Linux Capabilities是什么
通常来说,在 Linux 中,root 用户和普通的非特权用户之间权限差别是很大的。如果一个进程需要做只有root用户才能做的事情,它就必须运行 sudo 或者对应的二进制文件权限需要更改为"setuid root",但这就意味着它可以做任何root可以做的事情。从安全角度来看,这是不安全的,因为它的权限粒度不够细分,当被设定为"setuid root"的程序中发现漏洞时,这就会带来额外的风险。
这个问题导致了 Linux capabilities的引入。功能将root权限拆分为41个(在发布时)权限,这些权限可以单独授予进程或文件。其中一些权限是相当细粒度的,例如CAP_AUDIT_READ ,它控制读取审核日志的权限。其他的则范围很广,例如CAP_SYS_ADMIN ,它授予各种特权操作,例如在主机上挂载和卸载文件系统的能力。
探索Capabilities
与namespaces一样,标准 Linux 守护程序和实用程序在容器化之外使用capabilities。我们可以使用 libcap-ng-utils 包中的 pscap 程序来查看已向主机上的进程授予了哪些功能。pscap 将仅列出已被授予任何capabilities的程序;不会列出非特权进程。
pscap显示这些程序的capability
屏幕截图显示了 Ubuntu 22.04 虚拟机的pscap输出。虽然部分的进程以完整的capabilities运行,但其他进程的capabilities有限。 我们还可以使用同一软件包中的 filecap 程序来将capabilities添加到特定程序。向程序添加capabilities是将这些程序设置为root权限的一个很好的替代方法,因为可能攻击者会利用其中一个程序中的漏洞,使用capabilities将有助于降低权限提升的风险。
运行 sudo filecap -a 2>/dev/null 将显示系统上应用了capabilities的所有程序。(顺便说一句,2>/dev/null该命令的一部分只是一种阻止错误行打印出来的方法,从而更容易看到我们正在寻找的信息。
使用 filecap 显示具有capability的文件
从输出中,我们可以看到 ping 实用程序的多个实例已被授予创建 ping 常用的 Internet 控制消息协议 (ICMP) 数据包所需的net_raw功能 (CAP_NET_RAW)
让我们看一个示例,说明如何使用capabilities来允许程序执行需要提升权限的操作。我们将创建一个简单的 Golang Web 服务器,它侦听端口 80 并回复请求。如果我们编译它,然后尝试运行它,我们会收到一条 permission denied 的错误消息。
普通用户无法绑定80/TCP端口
现在让我们看看如果我们随后使用 setcap 该程序将 CAP_NET_BIND_SERVICE 添加到二进制文件并再次运行它会发生什么。错误不再发生,程序成功运行。
通过增加capability,Web 服务可以绑定端口 80/TCP
Capabilities和容器
现在我们已经了解了如何在 Linux 系统上使用这些capabilities,让我们来看看它们在容器中的使用方式。我们可以先在主机上运行一个容器 docker run -d nginx,然后再次使用 pscap 以查看发生了哪些更改。
pscap 显示了在容器中运行的 nginx 的capability
我们可以看到,我们的 ngin 进程已经被赋予了一组capabilities。此屏幕截图显示了 Docker 默认授予容器的capabilities集。此集旨在使大多数工作负载能够在容器内成功运行,同时限制可能导致权限升级攻击的任何功能。
还可以从容器内部检查容器具有哪些capabilities。您可以使用amicontaind等工具来进行检测,这将显示我们从容器外部看到的相同capabilities集。
amicontained 可用于显示容器内可用的capability
通过使用 Docker 的 cap-add 和 cap-drop 标志来添加和删除capabilities,可以增加或减少容器的权限。
最小化容器的Capabilities能力集
我们已经看到,默认情况下,Docker 容器提供了一组capabilities。根据您的应用程序,您可以删除部分或全部这些功能,以帮助加固容器。值得记住的是,capabilities是授予 root 用户的权限。这意味着,如果您的应用程序作为非 root 用户运行良好,它应该在没有任何capabilities的容器中正常运行。在这种情况下,您可以放弃所有capabilities。
还值得注意的是,在某些情况下,您认为需要的某些capabilities,但实际上可能不再需要。
第一个示例是ping 。正如我们前面提到的,ping使用NET_RAW功能。但是,根据您使用的 Linux 发行版,它实际上可能不再需要该功能。
让我们在 Ubuntu 22.04 安装上演示这一点。如果我们使用 getcap,我们可以看到ping具有NET_RAW功能。但是,如果我们使用 setcap 将其删除,然后再次ping尝试,它依然会起作用!
即使没有capability,ping 也可以正常工作
这是由于一项称为"ping_group_range"的功能,该功能允许 Linux 系统定义一系列组,这些组可以在没有权限的情况下发送 ICMP 请求并回显。当我们在测试 VM 上查看该设置时,我们可以看到他的生效范围包括几乎所有组,这解释了为什么ping能够在没有该功能 NET_RAW 的情况下工作。
sysctl 设置的ping 范围
在容器中,此设置可能因使用的运行时而异。它也可能与主机设置不同,因为特定网络命名空间中的设置不需要与主机设置保持一致。自 2020 年以来,Docker 默认启用此功能,因此任何最新版本都会在启动容器时创建的网络命名空间中自动设置 ping_group_range sysctl。从 1.18 版本开始,Kubernetes 已将其添加到"安全"sysctls 列表中,因此即使容器运行时未设置它,您也可以在 Pod 清单中添加此设置。
需要capabilities的另一个常见场景(NET_BIND_SERVICE,在本例中)是在“特权端口”集中绑定端口(默认情况下为 1024 以下的任何端口)时。例如,在创建绑定端口 80/TCP 的 Web 服务器时,这很有用。与 ping 的情况一样,有一个 sysctl 参数,您可以更改该参数以允许非特权进程绑定部分端口。在 Ubuntu 22.04 主机上,我们可以看到此参数 (net.ipv4.ip_unprivileged_port_start) 默认设置为 1024。这意味着,如果我们想要尝试绑定1024以下的任何端口,例如端口80,除非我们是 root 用户或进程具有该NET_BIND_SERVICE功能,否则它将无法工作。
非特权端口范围的 sysctl 设置
但是,在容器内部,情况可能会有所不同,具体取决于所使用的运行时。由于此设置(以及与网络相关的其他设置)可以在namespace级别进行管理,因此 Docker 内进行该操作仅影响被操作容器。在下面,您可以看到该参数net.ipv4.ip_unprivileged_port_start在 Docker 容器上默认设置为 0,允许非特权用户绑定低端口(1024以下)。
容器内非特权端口范围的 sysctl 设置
因此,我们已经看到,您很可能会从运行的容器中删除NET_RAW和NET_BIND_SERVICE,但是默认情况下授予 Docker 容器的所有其他capabilities怎么处理呢?答案是根据容器的需要,选择删除它们。在容器上测试capabilities的一种基本方法是一次性删除所有capabilities(即,通过使用cap-drop=ALL运行应用程序容器),然后在完成测试套件的运行时监视错误。这应该有助于确认容器是否确实需要任何特定capabilities来完成其工作。
结论
在这篇博文中,我们介绍了capabilities,这是 Docker 类容器使用的另一层隔离,用于将单个容器彼此分离以及与底层主机分开。capabilities是一种相对灵活的机制,它们可以提供的隔离可以根据应用程序操作的需要进行加强或削弱。我们还了解了如何使用capabilities作为在更精细的级别上授予 root 权限的一种方式,而不是像 setuid root 这样的旧单体权限结构。但是,capabilities无法解决的问题之一是阻止一个容器占用主机的所有资源。在本系列的下一部分中,我们将了解容器如何使用 cgroups 解决该问题。