在本系列的第一部分中,我们了解到容器实际上只是 Linux 进程。现在,我们需要了解容器如何与主机的其余部分隔离。换句话说,我们如何确保在一个容器中运行的进程不会轻易干扰另一个容器或底层主机的操作?
Linux 容器使用几种不同的机制来提供隔离,如下所示。这些层中的每一项能力都可以独立使用,其中命名空间是这篇文章将要重点阐述的内容。
与虚拟机相比,Linux 容器隔离的一个更强大的地方在于,它提供了控制隔离级别的灵活性。但是,这也可能导致安全漏洞。随着我们对容器隔离工作原理的了解越来越深入,我们将开始了解如何操作这些层以适应不同的场景。我们还将探讨如何使用标准的 Linux 工具与这些层进行交互并解决容器安全问题。
本文重点介绍隔离层中的第一层:命名空间(Namespace)。
Namespaces
Linux 命名空间允许操作系统为进程提供一个或多个系统资源的隔离视图。Linux 目前支持八种命名空间:
- -Mount
- -PID
- -Network
- -Cgroup
- -IPC
- -Time
- -UTS
- -User
命名空间是实现容器隔离的关键部分,因为它限制了包含的进程对主机其余部分的可见性。了解命名空间的工作原理也有助于保护容器和解决问题。命名空间非常灵活,因为它们可以单独或成组应用于一个或多个进程。还可以使用标准的 Linux 工具与它们进行交互,这为调试容器和对正在运行的容器实例执行安全检查提供了便利。
我们可以使用 lsns 命令查看主机上的命名空间,如下所示。此程序是大多数 Linux 发行版上 util-linux 软件包的一部分。
“NPROCS”字段显示 238 个进程正在使用此主机上的第一组命名空间。我们还可以看到,还有一些进程被分配给它们自己的命名空间(通常是mnt或uts)。这些进程不是由 Docker 启动的,但它们正在利用特定的命名空间来隔离其资源。
使用 Docker 命令docker run -d nginx启动新容器后,重新运行sudo lsns将显示 NGINX 进程的一组新命名空间(下面红线标识)。默认情况下,Docker 在创建容器时会使用mnt、uts、ipc、pid和net命名空间。
现在,我们已经简要介绍了命名空间,让我们更详细地了解每个单独的命名空间。
mount命名空间
mount(mnt)命名空间为进程提供了文件系统的隔离视图。它可用于确保进程不会干扰属于主机上其他进程的文件。使用该命名空间时,mnt会为进程提供一组新的文件系统挂载,以代替默认情况下的文件挂载。
我们可以通过查看 /proc 文件系统来查看进程使用了哪些挂载命名空间;该信息包含在 /proc/[PID]/mountinfo中。我们还可以使用像 findmnt 这样的工具,它将提供更好的展示各式。
使用这些类型的工具时,我们首先需要找到容器的进程 ID。一种方法是使用 Docker 的 inspect 命令。
运行docker inspect -f '{{.State.Pid}}' [CONTAINER]后会返回 PID 信息,然后我们运行findmnt -N [PID]就可以获取挂载信息。
在上面的屏幕截图中,我们可以看到我们的容器在/var/lib/docker中挂载了一个根文件系统,其中 Docker 存储了所有镜像和容器文件系统层。容器运行时使用 OverlayFS 来帮助提高容器的性能并降低存储要求。
需要记住的一个与安全相关的要点是,主机上的容器使用的所有根文件系统都将位于由容器运行时工具管理的目录中(默认情况下是/var/lib/docker/)。因此,您肯定希望确保该目录具有严格的文件系统权限,并且监控该目录是否存在未经授权的访问。
我们可以通过再次查看 /proc 来查看有关根文件系统的更多信息。可以看到,/proc/[PID]/mountinfo 包含有关提供给该进程的挂载的所有信息:
我们还可以使用其他 Linux 工具与 Docker 创建的命名空间进行交互。在对容器进行故障排除或调查容器中可能发生的恶意活动时,这是一种有用的技术。nsenter 就是这样一种非常有用的工具,它对于与命名空间的交互非常有用。我们可以使用它在容器内执行命令,而无需安装或使用 Docker CLI。Nsenter 应该在大多数 Linux 系统上可用,但如果未安装,通常可以将其添加为util-linux软件包的一部分。
该命令sudo nsenter --target 2525 --mount ls /将向我们显示容器的根文件系统,如下所示。此类信息在威胁搜寻或取证审查期间可能会有所帮助。
PID 命名空间
PID 命名空间允许进程具有主机上运行的其他进程的隔离视图。容器使用 PID 命名空间来确保它们只能查看和影响属于当前容器所包含应用程序的进程。多个容器也可以共享相同的 PID 命名空间。这对于故障排除很有帮助,因为你可以在与应用程序容器相同的命名空间中创建诊断容器,并使用它来在主应用程序进程上运行故障排除工具。
我们可以使用 nsenter 来显示容器内运行的进程列表。为此,我们需要一个具有 ps 二进制文件的容器映像,因为我们将输入 ps 需要获取进程列表的 pid 和 mnt 命名空间。(输入 mnt 命名空间的原因是,我们需要挂载 /proc 文件系统,以便允许 ps 获取该信息。
我们可以在后台将busybox镜像作为容器运行docker run --name busyback -d busybox top(这会在容器中运行程序top,因此它不会退出)。然后,我们将使用docker inspect获取容器的PID,并使用 nsenter 检查容器内的进程列表,如下所示。这使我们能够看到我们的top进程正在运行。
演示 PID 命名空间的另一种方法是使用 Linux 的取消pid共享,并在一个新的命名空间中运行程序。
执行sudo unshare --pid --fork --mount-proc /bin/bash命令将为我们提供一个新的 PID 命名空间中的 bash shell。
运行容器时,使用 PID 命名空间查看在另一个容器中运行的进程也很有帮助。docker run 上的 --pid 参数允许我们在另一个容器的进程命名空间中启动一个容器以进行调试。
为了演示这一点,我们将通过运行 docker run -d --name=webserver nginx 来启动 Web 服务器容器。然后,我们通过运行 docker run -it --name=debug --pid=container:webserver raesene/alpine-containertools /bin/bash 来启动调试容器。如果我们随后运行 ps -ef 命令,我们可以看到原始 Web 服务器容器中的进程以及调试容器中的进程。
在 Kubernetes 集群中,也可以跨容器共享进程命名空间,这对于调试问题很有用。如果要在 Pod 之间共享命名空间,则需要在启动要调试的容器时传递一个参数。具体来说,您需要在pod选项中包含shareProcessNamespace:true,如 Kubernetes 文档中所述。
网络命名空间
命名空间列表中的下一个是 network(net)命名空间。它负责提供进程的网络环境(接口、路由等)。它对于确保包含的进程可以在不相互干扰的情况下绑定它们所需的端口以及验证流量是否可以定向到特定应用程序非常有用。
与前面提到的命名空间一样,可以使用标准 Linux 工具(如 nsenter)与网络命名空间进行交互。第一步是获取容器的 PID,这样我们就可以使用 nsenter 来查看容器的网络。这一次,我们将使用 nsenter 上的-n参数进入网络命名空间,然后我们可以使用标准工具来显示容器的 IP 地址,如下图所示。
这里很重要的一点是,我们正在运行的ip程序来自主机VM,而不是在容器中。这使它成为一种有用的技术,用于解决未安装大量应用程序的特定容器中的网络查看问题,也就是我们不必在容器中安装ip应用也可以执行相应的功能。
另一个可用于与网络命名空间交互的 Linux 工具是 ip 命令本身,通过 netns 子命令。此子命令通常允许您与系统上的各种网络命名空间进行交互。但是请注意,它在 Docker 中不起作用,因为缺少netns 所依赖的动态链接库。
可以使用 Docker 来共享网络命名空间,类似于让容器共享 PID 命名空间。我们可以启动一个调试容器,也许安装了 tcpdump 等工具,并将其连接到正在运行的容器的网络。
运行 docker run -it --name=debug-network --network=container:webserver raesene/alpine-containertools /bin/bash 将允许我们连接到名为“webserver”的现有容器的网络命名空间。启动后,我们可以运行 netstat -tunap 来查看侦听端口,它将在调试容器中显示在端口 80 上运行的 Web 服务器。
在 Kubernetes 环境中,网络命名空间共享通常会针对单个 Pod 内的所有容器进行。虽然您无法在现有 Pod 中启动调试容器,但您可以使用新的临时容器功能将容器动态添加到 Pod 的网络命名空间中。我们可以通过使用 NGINX 镜像启动一个 pod,然后使用命令kubectl debug将一个临时容器添加到 pod 来演示其工作原理。正如我们在下面的屏幕截图中看到的,临时容器可以访问原始容器的网络命名空间。
这里需要注意的一个有趣的点是,在netstat输出的结果中,我们可以看到 PID 信息不可用。这是因为我们只共享了原始容器的网络命名空间,而不是 PID 命名空间。
还可以使用kubectl debug 在 Pod 中共享特定容器的命名空间。在 pod 中添加--target参数并指定对应容器将允许 kubectl 将调试容器设置为共享该指定容器的PID命名空间。
从下面的屏幕截图中可以看出,“PID/程序名称”列现在可以显示有关正在运行的 NGINX 程序的信息。
Cgroup 命名空间
Control groups(cgroups 旨在帮助控制进程在 Linux 系统上的资源使用情况。在容器中,它们用于降低“干扰邻居”(使用大量资源的容器,会降低同一主机上其他容器的性能)的风险。
一般情况下,分配给进程的 cgroup 没有命名空间,因此存在有关进程的信息从一个容器泄漏到另一个容器的风险。这也就是 cgroup 命名空间的引入的原因,它为容器提供了自己的隔离cgroup。
通常,在运行容器时不需要修改 cgroup 命名空间,但出于演示目的,让我们看看如果修改容器上的 cgroups 命名空间设置会发生什么。
首先,我们将启动一个容器并查看 :/sys/fs/cgroup
但是,如果我们创建另一个使用主机的 cgroup 命名空间的容器,我们可以在该文件系统中看到更多信息:
在查看可以访问主机的 cgroup 命名空间的容器的/sys/fs/cgroup/system.slice/目录时,我们可以看到它包含有关主机上运行的系统服务的信息。这是通过使用隔离的 cgroup 命名空间来缓解的信息泄漏类型的示例。
IPC 命名空间
IPC 命名空间与许多用例无关,但默认情况下在容器运行时上启用它,以便为某些类型的资源(如 POSIX 消息队列)提供隔离。
UTS 命名空间
UTS 命名空间是另一个不太常用的命名空间,它的用途是:设置进程使用的主机名。默认情况下,Linux 容器运行时会自动激活此命名空间,这就是容器具有与其基础 VM 不同的主机名的原因。
我们可以通过启动一对容器来演示这一点。第一个使用自己的 UTS 命名空间,第二个共享主机的 UTS 命名空间(使用--uts=host标志)。正如你在下面看到的,在第一个容器中,我们得到一个随机分配的主机名,在第二个容器中,我们的主机名与底层主机的主机名匹配。
Time命名空间
time命名空间是在 2020 年添加的,使其成为一个相对较新的 Linux 命名空间。它允许进程组具有与基础主机不同的时间设置,这对于某些目的很有用,例如在创建容器快照然后还原时测试或停止时间继续走动。
目前,并非所有容器运行时都支持它。Docker、containerd 和 CRI-O 使用的 Runc 正在为其规范添加支持。但是,如果要在容器中使用time命名空间,则需要Linux 容器(LXC)支持。
我们也可以使用 unshare 命令演示时间命名空间。在下面,您可以通过先检查没有time命名空间的主机的正常运行时间,然后在启动新的time命名空间时修改分配给进程的启动时间来查看效果。
User命名空间
User命名空间允许隔离运行进程的用户帐户等内容。最重要的是,从安全角度来看,它允许进程在命名空间内是 root 用户,而不是实际上在主机上是 root 用户。这在容器化中特别有用,因为某些应用程序需要 root 才能运行(例如,某些包管理器)。您可以使用User命名空间来启用这些应用程序,而不会引入以主机的 root 用户身份运行包含的进程的风险(许多容器运行时的常见默认设置)。
可以在容器运行时(如 Docker)上启用User命名空间。在其他运行时上,例如 Podman,默认情况下已启用此功能。目前,在 Kubernetes 中无法使用User命名空间,但正在努力解决这个问题。
我们可以通过再次使用unshare取消共享来演示User命名空间的效果。运行该命令将带我们进入一个新的 shell,在该 shell 中,我们似乎是 root 用户。unshare --fork --pid --mount-proc -U -r bash
鉴于我们没有使用sudo运行该命令,这似乎是一个糟糕的权限提升案例。但是,如果我们在机器上启动另一个 shell 并查看进程列表,我们可以看到由 unshare 命令启动的 bash shell 仍然以我们的原始用户身份运行,而不是 root。
此外,如果我们尝试删除只有 root 用户才能访问的文件,它将失败。
如果您尝试以非 root 用户身份启动新的用户命名空间,则该命名空间不起作用,则此功能可能在主机级别被阻止。此功能可能会在某些 Linux 发行版上被禁用,因为最近存在一些安全漏洞,例如 CVE-2022-0185,如果用户能够创建新的用户命名空间,这些漏洞最容易被利用。您可以通过查看kernel.unprivileged_userns_clone sysctl 的值来验证这一点。如果设置为“1”(如下所示),则启用该功能。如果设置为“0”,则非特权用户将无法在不使用 sudo 之类的内容的情况下创建新的用户命名空间。
结论
Linux 命名空间是 Docker 等容器运行时工作方式的基础部分。我们已经了解了它们如何以多种方式为容器的主机资源视图提供细粒度隔离。而且,由于它们是原生 Linux 功能,因此我们可以使用常见 Linux 发行版附带的工具与它们进行交互,从而帮助进行故障排除。
但是,仅命名空间并不能提供 Linux 容器如何与主机隔离的完整答案。请阅读本系列的下一部分,我们将研究如何在 Linux 中实现Capabilities,以及它们如何限制 Linux root用户的权限。我们还探讨了 Docker 如何使用Capabilities来确保即使在容器中成为 root 用户也无法从容器逃逸并破坏主机:容器安全探索第 3 部分:Capabilities。