前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >命名空间介绍之五:用户命名空间

命名空间介绍之五:用户命名空间

作者头像
谛听
修改2019-11-05 10:34:46
3K0
修改2019-11-05 10:34:46
举报
文章被收录于专栏:巫山跬步巫山跬步

继续我们的命名空间系列文章,本文看一下用户命名空间,大部分实现于 Linux 3.8。(剩余的工作是 XFS其它文件系统中的一些改动;后者合并于 3.9)。用户命名空间与用户和组 ID 相映射。这意味着一个进程在某个用户命名空间内的用户和组 ID 可以与用户命名空间外的不同。最重要的是,一个进程可以在一个命名空间外有一个非 0 的用户 ID ,同时在命名空间内有一个为 0 的用户 ID;换句话说,进程在一个用户命名空间外没有特权,但在用户命名空间内有 root 特权。

创建一个用户命名空间

用户命名空间被带有 CLONE_NEWUSER 标志的 clone() 或 unshare() 创建。自 Linux 3.8 开始(不像用来创建其它类型的命名空间的标志),创建一个用户命名空间并不需要特权。在接下来的例子中,所有的用户命名空间都被 ID 为 1000 的非特权用户创建。

为了开始探究用户命名空间,我们将看一个小例子 demo_userns.c,该程序在新的用户空间中创建了一个子进程。该子进程仅展示其有效用户和组 ID 及 capabilities。以非特权用户运行该程序,输出如下:

$ id -u          # Display effective user ID of shell process
1000
$ id -g          # Effective group ID of shell
1000
$ ./demo_userns 
eUID = 65534;  eGID = 65534;  capabilities: =ep

程序的输出展示了一些有趣的细节。其中之一是被分配给子进程的 capabilities。字符串“=ep”(库函数 cap_to_text() 产生,可将 capability 转为文本)说明即便该程序被非特权账户运行,该子进程仍有全部的权限和有效的 capabilities。当一个用户命名空间被创建,其内的第一个进程将被赋予该命名空间中的所有权限。这允许该进程在命名空间内的其它进程创建之前,执行该命名空间内任意必需的初始化操作。

第二个有趣的地方是子进程的用户和组 ID。之前提过,一个进程的用户和组 ID 在一个用户命名空间内、外可以不同。但是,需要将用户命名空间内的用户 ID 映射到用户命名空间外的用户 ID;组 ID 也一样。这样,当一个在用户命名空间内的进程执行影响系统范围较大的操作时,系统可以执行恰当的权限检查。

返回进程的用户和组 ID 的系统调用 --- 例如,getuid() 和 getgid() --- 总是返回调用进程所在的用户命名空间内的凭证。如果一个用户 ID 没有映射到用户命名空间内,那么系统调用会返回 /proc/sys/kernel/overflowuid 文件中的值,在标准系统中默认为 65534。最初,一个用户命名空间并没有用户 ID 映射,所以命名空间内的用户 ID 均映射到该值。同样,一个新的用户命名空间没有对应于组 ID 的映射,所有没有映射的组 ID 都会映射到 /proc/sys/kernel/overflowgid(与 overflowuid 默认值相同)。

还有一个上述输出未能体现的重点。尽管新进程拥有新用户命名空间中的所有权限,但在父命名空间中没有 capabilities。无论调用 clone() 的进程的 capabilities 和凭证是什么样的。尤其是,即便是 root 调用 clone(CLONE_NEWUSER),子进程在父命名空间中也没有 capabilities。

最后一点是,命名空间可以嵌套;也就是说,每个用户命名空间(最初的用户命名空间除外)都有一个父用户命名空间,并且可以有 0 个或多个子用户命名空间。用户命名空间中的进程可通过调用带有 CLONE_NEWUSER 标志的 clone() 或 unshare() 生成当前用户命名空间的子用户命名空间。接下来阐述用户空间的父-子关系。

映射用户和组 ID

通常,创建一个新用户命名空间后的第一步是定义一个用户和组 ID 的映射,将被该命名空间内的新进程使用。这可通过将映射信息写入对应于用户命名空间中某个进程的 /proc/pid/uid_map 和 /proc/pid/gid_map 文件来完成。(最初,这两个文件是空的。)此信息由一行或多行组成,每行包含三个用空格分隔的值:

ID-inside-ns   ID-outside-ns   length

ID-inside-ns 和 length 值一起定义命名空间内 ID 的范围,这些 ID 范围将映射到命名空间外相同长度的 ID 范围。ID-outside-ns 值指定外部范围的起点。如何解释 ID-outside-ns 取决于打开文件 /proc/PID/uid_map(或 /proc/PID/gid_map)的进程是否与进程 PID 在同一个用户命名空间中:

  • 如果两个进程位于同一命名空间中,则 ID-outside-ns 为进程 PID 的父用户命名空间中的用户 ID(组 ID)。常见情况是,进程正在写入自己的映射文件(/proc/self/uid_map 或 /proc/self/gid_map)。
  • 如果两个进程位于不同的命名空间中,那么 ID-outside-ns 为打开 opening/proc/pid/uid_map(/proc/pid/gid_map)的进程所在用户命名空间中的用户 ID(组 ID)。然后,该写进程将定义自己用户命名空间的映射。

再次调用 demo_userns 程序,但这次只调用一个命令行参数(任何字符串)。程序会循环,每隔几秒显示凭证和 capabilities:

$ ./demo_userns x
eUID = 65534;  eGID = 65534;  capabilities: =ep
eUID = 65534;  eGID = 65534;  capabilities: =ep

现在,切换到另一个终端窗口 --- 在另一个命名空间(即运行 demo-userns 进程的父用户命名空间)中运行 shell 的终端,并在 demo-userns 创建的新用户命名空间中为子进程创建一个用户 ID 映射:

$ ps -C demo_userns -o 'pid uid comm'      # Determine PID of clone child
    PID   UID COMMAND 
    4712  1000 demo_userns                    # This is the parent
    4713  1000 demo_userns                    # Child in a new user namespace
$ echo '0 1000 1' > /proc/4713/uid_map

回到运行 demo_userns 的窗口,可以看到:

eUID = 0;  eGID = 65534;  capabilities: =ep

父用户命名空间中的用户 ID 1000(之前映射到 65534)已映射到 demo-userns 创建的用户命名空间中的用户 ID 0。自此,新用户命名空间中处理此用户 ID 的所有操作都将看到数字 0,而父用户命名空间中相应的操作将仍然看到用户 ID 1000。

我们同样可以创建新用户命名空间中组 ID 的映射。切换到另一个终端窗口时,我们为父用户命名空间中的组 ID 1000 创建一个到新用户命名空间中的组 ID 0 的映射:

$ echo '0 1000 1' > /proc/4713/gid_map

回到运行 demo_userns 程序的窗口,可以看到该改变反映在了有效用户组 ID 上。

eUID = 0;  eGID = 0;  capabilities: =ep

写映射文件的规则

写 uid_map 文件时会有一些规则;类似的规则也适用于写 gid_map 文件。最重要的规则如下。

每个命名空间的映射只能被定义一次:对用户命名空间的一个进程的 uid_map 文件仅能执行一次写入(可能包含多个以换行符分隔的记录)。此外,目前可以写入文件的行数限制为 5 行(将来可能会有其它限制)。

/proc/PID/uid_map 文件由创建命名空间的用户 ID 拥有,并且只能由该用户(或特权用户)写入。此外,必须满足以下所有要求:

  • 写进程必须在进程 PID 的用户命名空间中具有 CAP_SETUID(gid_map 为 CAP_SETGID)capability。
  • 无论什么 capabilities,写进程都必须位于进程 PID 的用户命名空间中或进程 PID 的父用户命名空间中。
  • 以下条件必须满足: - 写入 uid_map(gid_map)的数据由一行组成,该行将(仅)父用户命名空间中写进程的有效用户 ID(组 ID)映射到用户命名空间中的用户 ID(组 ID)。此规则允许用户命名空间中的初始进程(即 clone() 创建的子进程)为自己的用户 ID(组 ID)写映射。 - 如果进程在父用户命名空间中具有 CAP_SETUID(gid_map 为 CAP_SETGID)capability,则可以定义父用户命名空间中的任意父用户 ID(组 ID)的映射。如前所述,新用户命名空间中的初始进程在父命名空间中没有任何 capabilities。因此,只有父命名空间中的进程才能编写父用户命名空间中 ID 的映射。

Capabilities, execve(), 和用户 ID 0

本系列的前面文章中,开发了 ns_child_exec 程序。该程序使用 clone() 在新命名空间中创建一个子进程,并在子进程中执行一个 shell 命令。

使用该程序在一个新用户空间中执行一个 shell,然后在该 shell 中定义新用户命名空间的用户 ID 映射。这样的话,会有如下问题:

$ ./ns_child_exec -U  bash
$ echo '0 1000 1' > /proc/$$/uid_map       # $$ is the PID of the shell
bash: echo: write error: Operation not permitted

该错误是因为 shell 没有新用户命名空间中的 capabilities,从下面命令的输出可以看出:

$ id -u         # Verify that user ID and group ID are not mapped
65534
$ id -g
65534
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000

在执行 bash shell 的 execve() 调用中出现该问题:当有非零用户 ID 的进程执行 execve() 时,将清除该进程的 capability (Capabilities(7) 手册页详细介绍了 execve() 对 capabilities 的处理。)

为避免此问题,必须在执行 execve() 之前在用户命名空间内创建用户 ID 映射。这在 ns_child_exec 程序中是不可能的;为此,我们需要一个增强版的程序。

userns_child_exec.c 程序执行与 ns_child_exec 程序执行相同的任务,并有相同的命令行界面,但它可有两个附加的命令行选项 -M 和 -G。这些选项接受用于定义新用户命名空间的用户和组 ID 映射的字符串参数。例如,以下命令将新用户命名空间中的用户 ID 1000 和组 ID 1000 映射到 0:

$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash

这次,成功地更新了映射文件,并且看到了 shell 输出了预期的用户 ID,组 ID 和 capabilities:

$ id -u
0
$ id -g
0
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff

userns_child_exec 程序中有一些巧妙之处。首先,父进程(clone() 的调用者)或子进程可以更新新用户命名空间中的用户 ID 和组 ID 映射。然而,根据上述规则,子进程只能定义自身有效用户 ID 的映射。只有父进程可以定义子进程的任意用户和组 ID 的映射。此外,父进程必须具有适当的 capabilities,即 CAP_SETUID、CAP_SETGID 和(父进程打开映射文件所需的权限)CAP_DAC_OVERRIDE。

此外,父进程必须在子进程调用 execve() 之前更新映射文件(否则我们就遇到了上面描述的问题,在调用 execve() 期间子进程将失去 capabilities)。为此,这两个进程需使用一个管道进行同步;程序源代码中有注释。

查看用户和组 ID 映射

到目前为止的示例展示了通过 /proc/PID/uid_map 和 /proc/PID/gid_map 文件来定义映射的用法。还可用这些文件查看控制进程的映射。当写入这些文件时,第二个(ID-outside-ns)值的解释取决于打开文件的进程。如果打开文件的进程与进程 PID 在同一个用户命名空间中,则 ID-outside-ns 是关于父用户命名空间定义的。如果打开文件的进程位于不同的用户命名空间中,则会根据打开文件的进程的用户命名空间定义 ID-outside-ns。

我们可以通过创建几个运行 shell 的用户命名空间,并检查命名空间中进程的 uid_map 文件来说明这一点。首先,使用运行 shell 的进程来创建新的用户命名空间:

$ id -u            # Display effective user ID
1000
$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
$ echo $$          # Show shell's PID for later reference
2465
$ cat /proc/2465/uid_map
            0       1000          1
$ id -u            # Mapping gives this process an effective user ID of 0
0

切换到另一个终端窗口,创建一个使用不同用户和组 ID 映射的同级用户命名空间:

$ ./userns_child_exec -U -M '200 1000 1' -G '200 1000 1' bash
$ cat /proc/self/uid_map
        200       1000          1
$ id -u            # Mapping gives this process an effective user ID of 200
200
$ echo $$          # Show shell's PID for later reference
2535

继续留在第二个终端窗口,该终端运行在第二个用户命名空间,看一下另一个用户命名空间中进程的用户 ID 映射:

$ cat /proc/2465/uid_map
             0        200          1

上述输出显示,另一个用户命名空间中的用户 ID 0 映射到此命名空间中的用户 ID 200。注意,同一个命令在另一个用户命名空间中执行时输出不同,因为内核根据从文件中读取的用户命名空间来生成 ID-outside-ns 值。

回到第一个终端,查看第二个用户命名空间中进程的的用户 ID 映射,可以看到如下相反的映射:

$ cat /proc/2535/uid_map
        200          0          1

再次,此处的输出与执行于另一个用户命名空间中的相同命令的输出不同,因为 ID-outside-ns 值是根据从文件中读取的进程的用户命名空间生成的。当然,第一个命名空间中的用户 ID 0 和第二个用户命名空间中的用户 ID 200 均映射到最初的命名空间中的用户 ID 1000。可以在运行于最初的用户命名空间中的第三个 shell 执行如下命令:

$ cat /proc/2465/uid_map
            0       1000          1
$ cat /proc/2535/uid_map
        200       1000          1

结束语

本文中,我们看了用户命名空间的一些概念:创建一个用户命名空间,使用用户和组 ID 映射文件的用法,以及与用户命名空间和 capabilities 的交互。

如之前的文章所述,实现用户命名空间的动机之一是让非 root 应用程序访问以前仅限于 root 用户的功能。在传统的 UNIX 系统中,为了防止非特权用户操纵特权程序的运行时环境(这可能会以意外或不希望的方式影响这些程序的操作),各种功能都仅限于 root 用户。

用户命名空间允许进程(在命名空间之外没有权限)具有 root 权限,同时将该权限的范围限制在命名空间,结果是进程无法在更大的系统中操作特权程序的运行时环境。为了有意义地使用这些 root 特权,我们需要将用户名空间与其他类型的名称空间结合起来,该主题将构成本系列下一篇文章的主题。


原文:https://lwn.net/Articles/532593/

公众号:Geek乐园

博客:https://blog.csdn.net/u012319493/article/details/102819429

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 创建一个用户命名空间
  • 映射用户和组 ID
  • 写映射文件的规则
  • Capabilities, execve(), 和用户 ID 0
  • 查看用户和组 ID 映射
  • 结束语
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档