当主机上运行多个进程时,管理系统资源可能是一个挑战。单个行为异常的程序可能会消耗所有可用资源,从而导致整个系统崩溃。为了解决这个问题,Linux 依靠控制组 (cgroups) 来管理每个进程对资源(如 CPU 和内存)的访问。
Docker 和其他容器化工具使用 cgroups 来限制容器可以使用的资源,这有助于避免相互干扰的问题。这在使用 Kubernetes 时特别有用,因为来自多个应用程序的工作负载经常在同一主机上共享资源。
在这篇文章中,我们将仔细研究 cgroups,并探讨它们如何确保每个进程都能访问高效运行所需的资源。我们还将介绍 cgroups 的几个安全方面,包括如何使用 cgroups 来降低拒绝服务攻击的风险,并管理容器对主机上特定设备的访问。
cgroups v1 和 v2
值得注意的是,根据Linux发行版和版本,在给定的主机上可能会使用两个版本的cgroups。与原始实现相比,Cgroup v2 提供了管理优势,并且是某些容器功能(如非root容器)所必需的。
Cgroup v2 最初是在2016年 4.5 版本的 Linux 内核中引入的,但直到最近才成为某些发行版的默认版本。要确定主机上运行的版本,可以验证挂载的文件系统。例如,如果您在 Ubuntu 20.04 主机上执行该命令 mount | grep cgroup ,您将看到一行展示“cgroup2”,另几行展示“cgroup”,这表示两个系统都已安装。
cgroup 挂载列表 - Ubuntu 20.04
但是,如果您在 Ubuntu 22.04 系统上运行相同的命令,则只会看到 cgroup v2。
cgroup 挂载列表 - Ubuntu 22.04
由于 cgroup v2 是最近 Linux 发行版中使用的版本,因此我们将在示例的其余部分重点介绍 v2。
Cgroups 基础知识
有几种方法可以检查 Linux 主机上使用的 cgroup。一种选择是使用 /proc 文件系统来查看用于特定进程的 cgroup(例如,正在运行的用户的 bash shell)。
执行命令 cat /proc/[PID]/cgroup 将显示进程所属的 cgroup “slice” 和 “scope”(slice 和 scopes 用于组织 cgroup 和进程)。在以下示例中,我们首先用 ps -fC bash 获取 shell 的进程 ID。然后,我们使用该进程 ID 来发现它使用的 cgroup 会话。
查找 bash shell 的 cgroup 作用域和切片
要查看可以为该进程修改的可用资源,您可以查看 /sys/ 文件系统,它对应于我们从上一个命令中获得的信息(例如:sys/fs/cgroup/user.slice/user-1000.slice/session-4.scope)
显示给定 cgroup 范围的资源
此手动过程可能非常耗时,因此您可以利用更便捷的工具,以更有条理的方式显示 cgroup 信息。例如,systemd-cgls 可以显示主机上不同 cgroup 作用域的分层视图。
使用 systemd-cgls 获取 cgroup 信息的分层视图
此外,cgroup-utils 软件包中的 lscgroup 实用程序可用于检查 cgroup 信息。
使用 lscgroup 显示 cgroup 信息
使用 cgroups 限制资源
现在我们已经了解了如何查看 cgroup 信息,下一步是探索如何使用 cgroups 来限制进程可用的资源,这有助于缓解拒绝服务风险。为了证明这一点,我们将使用 stress 工具来模拟攻击者或行为不端的应用程序消耗我们主机上的所有 CPU。
在 Docker 容器中,我们可以利用命令 stress -c 2 ,它将启动两个进程,总共消耗 2 个 CPU 内核。然后,通过在另一个窗口中执行命令 top ,我们可以验证对主机 CPU 的影响。
使用压力工具消耗 CPU 资源
Docker 提供了各种选项来限制容器可以使用的 CPU 时间量,但最简单的是--cpus标志,它允许您指定可以使用的 CPU 的十进制数。在幕后,Docker 利用 cgroups 来强制执行此限制。
例如,执行 docker run --name stress --cpus 0.5 -it stressimage /bin/bash 会将容器限制为 0.5 个 CPU。使用上一个容器中的相同 stress 命令后,我们可以在 top 中观察到此限制的结果。
CPU 资源使用率受 cgroups 限制
现在,stress 进程不再能够利用两个 CPU 内核,而是限制为 0.5 个 CPU(每个进程占内核的 25%)。
我们还可以通过检查底层文件系统来观察 Docker 实现的 cgroup 限制的细节。为此,我们首先使用 docker inspect -f '{{.State.Pid}}' stress 获取 Docker 容器的进程 ID。然后我们可以查找此过程的 cgroup 信息。容器的 cgroup 目录包含一个 cpu.max 文件,其值为 50000 100000 ,相当于 0.5 个 CPU。默认情况下,Docker 不限制进程的 CPU 使用率,因此文件将显示值 max 100000 。如果攻击者有权访问此容器,则可以使用主机上的所有 CPU 资源(例如,挖掘加密货币)。
使用 cgroups 限制fork炸弹
Linux 系统上常见的拒绝服务攻击称为frok炸弹,当攻击者生成大量进程,最终耗尽系统资源时,就会发生这种攻击。默认情况下,容器(和其他 Linux 进程)在它们可以生成多少个新进程方面不受限制,这意味着任何进程都可以创建fork炸弹。
Cgroup 能够限制可以生成的进程数量,从而有效地保护主机免受fork炸弹攻击。我们可以使用 docker run 的--pids-limit 参数来演示这一点,这实质上将设置适当的 cgroup。
要了解其工作原理,我们可以使用 docker run -it --pids-limit 10 ubuntu:22.04 /bin/bash 命令启动容器,这会将容器限制为最多 10 个进程。然后我们可以使用命令执行 bash fork 炸弹 :(){ :|: & };:
Docker PID 限制在fork炸弹中生效
很快,容器达到 10 个进程的限制,并显示错误。但是,底层主机将保持响应,从而防止拒绝服务攻击。
使用 cgroups 控制设备访问
cgroups 的另一个与安全相关的方面是,它们可用于控制对设备的访问。容器提供对主机上一系列设备的访问,详见 runc 的允许设备列表,并且可以利用 Docker 的功能(使用 cgroups)将其他设备添加到该列表中。这允许您向特定容器授予对硬件(例如音频设备)的访问权限。
您可以将 --device 选项添加到命令 docker run 中以授予对设备的访问权限。例如,执行 docker run -d --rm --device /dev/dm-0 --name webdevice nginx 会生成一个有权访问/dev/dm-0设备的容器。
与 CPU 或内存等其他资源相比,Linux 工具没有提供那么多的功能来检查 cgroup 对设备的访问。在 cgroup v2 中,一般使用 eBPF 程序用于管理对设备的访问,因此标准工具将不起作用。所以, bpftool 是必需的。您可以使用此程序列出与任何给定 cgroup 关联的 eBPF 程序,从而提供容器对主机访问的一些可见性,尽管不是很详细。
结论
控制共享资源是确保多个容器可以有效地共享单个服务器的关键。在这篇文章中,我们介绍了 cgroup,这是 Linux 系统用于实现此控制的主要机制。我们还演示了如何利用 cgroup 来帮助缓解常见的拒绝服务攻击,并管理对连接到主机的特定设备的访问。
到目前为止,我们检查的所有安全机制都在系统上的 root 用户的控制之下。但是,有时我们想要限制 root 用户的操作,那又该怎么办呢?在下一篇文章中,我们将探讨 SELinux 和 AppArmor 等强制访问控制(MAC)系统如何实现这一目标。