2019年 12月 2日Scott McCarty
您是以 root 身份运行容器,还是以普通用户身份运行容器?这是一个看似简单的问题。您可能立刻就会给出答案您是否真的很清楚威胁模型?我觉得您可能并不了解。这篇文章旨在帮助您详细了解。
回答上述问题之前,您需要确定我们谈论的容器引擎(Podman、Docker、CRI-O、containerd 等)、容器内的进程(apache、postgresql、mysql 等)还是容器映射到的进程 ID(三者可以不同)。乍一听,这可能并不明显。容器引擎或其在容器中的子流程几乎可以以任何用户身份运行。
了解 Root
随着 Podman 的出现,无根容器(rootless containers)成为现实。由于 Podman 创建容器作为自身的直接子流程,因此很容易证明有四种可能的选项需要考虑。但是,什么是无根?若要了解无根,您必须了解容器内的 root。若要了解容器内的 root,您必须了解容器外部的 root。很好,非常好。
下表显示了容器内部和外部的 root(感谢 Vincent Batts 在 DevConf.us 2019 上明确这些概念)。借助这个框架,您的脑海中应该会开始形成对无根的理解:
我们来指出上述示例的一些有趣之处。首先,命令行选项有 -i
(交互式)、-t
(终端)和 -u
(用户)- 组合,使用这些选项在容器内提供交互式终端,并指定容器化进程应当以 sync 用户身份运行。您可以通过键入 ‘man podman-run’ 命令来进一步研究这些内容。
其次,密切注意 $
,这表示我们的 shell 以普通用户身份运行,#
表示我们的 shell 以 root 身份运行。这些 Unix 传统将有助于解释容器内部和外部的 root。
第三,在上方示例中,Podman 根据定义在容器外部,以 root 或常规用户身份运行(fatherlinux),而在容器内,bash 以 root 或常规用户身份运行(sync)。容器主机上的 /etc/passwd 文件中的用户用于 Podman,而容器镜像中的 /etc/passwd 文件中的用户用于运行中的容器。换句话说,这两个 passwd 文件意味着我们可以有两组用户,一组在容器外部,一组在容器内部。这正是用户命名空间的作用。
用户命名空间
当内核在容器内创建运行中的进程时,用户命名空间将容器化进程的用户 ID 映射到容器外的其他用户 ID。我们来看一下:
命令行选项有 -i(交互式)、-t(分离)和 -u(用户)- 组合,这些选项在后台运行容器,并指定容器化进程应当以 sync 用户身份运行。就像之前一样,您可以通过键入 man podman-run
命令来进一步研究这些内容。
Podman 还提供了一个非常酷的子命令 top,它可以让我们将容器主机上的用户映射到运行中容器中的用户。上例演示了当我们以 root 身份运行容器时,我们将容器中的 sync 用户(uid 5)映射到底层容器主机上的 sync 用户(uid 5)。这意味着,如果进程脱离此容器,它可以使用真正的 sync 用户的特权运行。
另一方面,当我们以常规用户(fatherlinux)身份运行完全相同的容器时,它会将运行中容器中的 sync 用户(uid 5)映射到底层容器主机上的 uid 100004。等等!为什么不将 sync 用户(uid 5)映射到 fatherlinux(uid 1000)?
简单来说,是因为使用更新的内核和更新的 shadow-utils 软件包(useradd
、passwd
等),每个新用户都有一系列用户 ID 可用。传统上,在 Unix 系统上,每个用户只有一个 ID,但现在每个用户可以支配数千个 UID,以在容器内部使用。
这在容器使用多个用户时非常有用,例如:在单个容器或容器集中同时运行 Apache 和 MySQL,或者通过以其他用户身份运行的代理来运行 sidecar 容器。但是,这种映射从何而来?来自两个文件,/etc/subuid
和 /etc/subgid
。通过 usermod 命令添加用户或系统管理员手动添加用户时,将在这些文件中创建条目。
可选深入了解用户标识符
下方是我的系统上条目的一个示例。通过下列条目,fatherlinux 用户可以将容器中最多 65,535 个用户 ID 映射到系统上的真实用户 ID(从 100,000 开始)。默认情况下,shadow-utils(useradd、passwd 等)这一用户 ID 范围仅保留给一个用户。useradd 命令将为下一个用户保留下一个范围。在本例中,用户为 fred,用户 ID 从 165536 开始:
cat /etc/subuid fatherlinux:100000:65536 fred:165536:65536 cat /etc/subgid fatherlinux:100000:65536 fred:165536:65536
您还可以从容器内查看此映射:
注意,以 root 身份运行 Podman 时,容器中提供完整的用户 ID 范围(4294967295 == 32 位)。但是,当 Podman 以 fatherlinux 身份运行时,它会将容器内的 root 映射到 fatherlinux 用户(1000),并将 sync 用户(uid 5)映射到介于 100,000 和 165,535 之间的 UID。
这是一项出色的安全功能,因为现在容器引擎和运行中容器内的容器化进程都以不同的非特权用户身份运行。从 100,000 到 165,535 的用户 ID 集合在系统上没有特殊特权,即使是用户 fatherlinux(1000)也没有。这意味着,如果容器中的进程发生故障,它将在容器主机上受到严格限制。
随之而来的另一个问题是,如果添加一堆用户,系统会不会用完 UID?简单来说,是的。但是,这种情况不太可能发生,因为 UID 有 32 个数字,可以组合成 40 亿个 UID。这意味着,您最多可以添加 65,535 个用户到系统中(4294967295 除以 65535)。对于大多数用例而言,这应该足够了。
我们来深入探讨无根容器最后一个细微差别。/etc/subuid
文件用于将容器内的用户映射到容器外的用户,但用户(下例中为 fatherlinux)必须在容器镜像中定义,否则 Podman 不能启动容器:
podman run --user fatherlinux -it ubi8 bash
输出:
unable to find user fatherlinux: no matching entries in passwd file
您必须在容器镜像内的 /etc/passwd
文件中指定容器中的用户 ID。这是容器如何在本质上链接到容器内的操作系统并与容器主机操作系统保持隔离的又一个示例。容器基于 Linux。
容器深度防御
由于客户端服务器模型的原因,这个概念很难通过 docker
守护进程来理解。借助 docker
客户端服务器模型,我们可以以 root 身份运行容器,即使以普通用户身份运行命令也是如此。这是因为 docker
守护进程以 root 身份运行,因此它具有 root 的所有特权。这一点现在应该清楚得多。为了演示,请运行以下命令:
为确保运行容器的用户不会获得对您主机的 root 访问权限,您需要以非 root 用户身份运行容器引擎和容器化进程。这在服务(httpd
、MySQL 等)与操作系统中的特权资源之间提供了多个安全层。以非 root 用户身份运行容器引擎是一个防御层,而以不同的非 root 用户身份在容器中运行进程则提供另一层防御。
Dan Walsh 做的非常出色,本文将对此进行更深入的探讨:以非 root 用户身份运行无根 Podman。概括而言,Podman 等无根容器引擎允许您以用户帐户身份运行。然后,在容器内,您可以将一组虚拟用户映射到一组仅由您的帐户控制的用户 ID,用于容器化进程。
现在,您应该能够更全面地理解容器内部和外部的 root 权限了。