构造你自己的容器---4
金蝶云社区-墨家总院
墨家总院
81人赞赏了该文章 654次浏览 未经作者许可,禁止转载编辑于2019年01月15日 13:44:56

(本文独家发布在金蝶云社区上)

前言

几年前,当我还在我的上家公司工作的时候,因为一些保密的机制,我们工作用的电脑分为内网,外网。外网只是一台普通的电脑,可以连互联网,可以查看邮件。而内网是通过ssh或者vnc连接的服务器,通常一个team十几个人用一个服务器,而我们每个人都用一个私人账户。这个服务器由专门的IT部门负责。

每隔一段时间,我发现我可能会需要一个像valgrind这样的特定工具,但是这个工具不是现成的,或者是另一个已经存在的工具的最新版本,像gcc一样。(将“valgring”和“gcc”替换为“Node”,“Rust”或“Go”,你明白了。)

在那个时刻,我们基本上有两个选择。 首先,如果这个工具对我们的研究至关重要,IT部门可以为我们每个人安装它; 或者事实并非如此,我们只是在自己的随机项目里做一些实验。

在后面的case中,唯一的方案就是我们自己手动安装这个工具,甚至从源码编译开始。然后将可执行文件的路径加到了系统路径的变量中,这样就达到目的了。

一年前,我刚好接触到了Docker,当时有同事问我Docker是否可以解决这类问题。 我回答说这是一个危险的想法。 让docker访问用户基本上就像给他root密码一样。

现在正好是个机会借助docker来介绍一下第六种隔离技术,即user namespace。

User Namespaces 和 Docker

简而言之,用户命名空间是一种特殊的Linux内核机制,允许Docker容器拥有“伪造”的root用户。例如,容器中的root用户能够管理容器中的根用户拥有的文件,充当容器中的任何用户,管理他自己的网络接口和他的一些挂载点(限制适用)并同时在主机系统上用uid 1000去“映射”到用户”ubuntu”。

User namespace最早是在Linux 3.5的版本被引入内核,但是直到 
Linux 4.3才趋于稳定。

我不会在这里深入介绍用户命名空间的细节,虽然我非常喜欢更加底层一点的技术,但这远远超出了本文的范围。

现在开始我们的docker之旅,我的版本是Docker 1.0(可能不是最新的版本),它支持一个新的daemon的选项:–userns-remap=[USERNAME]。

那么–userns-remap和[USERNAME]具体是什么意思呢?

如前所述,用户命名空间的工作原理是将一些虚拟用户ID(如root)映射到主机上的其他用户ID, 因此选项名称叫做“–userns-remap”。

关于“[USERNAME]”,指的是“/etc/subuid”和“/etc/subgid”文件。总之,这些文件定义了给定用户可以使用的用户和组ID,除了他自己的用户ID。 就像root可以冒充任何用户ID一样。如果你想知道这个文件来自哪里,它来自useradd命令。每次在系统上创建真实用户(不是系统用户)时,都会分配65536个子ID。

不管怎样,它只是像这样维护简单的文本文件:

yadutaf:100000:65536
somuser:165536:65536
...

它的具体意思是:“让用户’yadutaf’从100000开始,总共可以使用65536 个uid。” 和 “让用户’someuser’从165536开始,可以使用65536个uid”。 这基本上是下一个相邻的65535个uid的范围。

这个规律没有被写到石头里,但是可以通过下面的公式预测出来:

FIRST_SUB_UID = 100000 + (UID - 1000) * 65536

另外这只是个转换,我们可以做一点不一样的事情:

yadutaf:1000:1
yadutaf:100000:65535

它的具体意思是:“让用户’yadutaf’从100000开始,可以使用自己的uid一样也是一共65535个。”而且这也没有破坏什么。但是这开始有点意思了。

当使用docker守护进程启动docker –userns-remap = yadutaf时,docker将解析yadutaf的subuid和subgid文件,通过增加start id对所有读取条目进行排序并生成内核用户映射规则。我们就不去深入细节了,这将在/proc/[PID]/uid_map中生成以下规则:

0       1000          1
1     100000      65535

是不是看起来有点熟悉,这个结构貌似跟上面的那个有点像,但意义稍有不同。这一次,它的意思是:

让容器外面的uid 1000 扮演容器里的root 
让容器外面的65535个uid从100000开始,并扮演容器里从1开始的65535个uid。

换句话说,1000将变成1, 100002将变成3。

这非常强大,因为这是在主机和容器之间共享文件而不失去对它们的访问权限的主要关键技术。你需要一个普通的uid。而这个普通的uid将会在容器中变成root。

把权力还给用户,没有妥协

有了以上所有准备,我们可以放到一起见证奇迹的发生了。我们需要:

  • 取到最近的docker的版本(大于1.10.0)

  • 配置subid以让我们的用户能够在容器里扮演root

  • 配置docker以便它使用我们的范围

  • 使用真实的应用程序

当然,当名字通过命令行传递到docker daemon的时候,只能使单一用户工作。但是请注意Docker 1.10是第一个支持这个功能的版本。而且这个版本将继续演进变得更加灵活。

让我们开始吧,假设我们的用户是“alexander”,uid是1000。我们将希望/etc/subuid 和 /etc/subgid 包含:

alexander:1000:1
alexander:100000:65535

而且我更希望docker可以使用它,而不是和systemd的units文件纠缠在一起。我们将用到docker的配置文件:/etc/docker/daemon.json:

{
        "userns-remap": "alexander"
}

然后我们需要做的是重启docker daemon,运行一个赶紧,随机的测试容器,来看看结果:

$ sudo systemctl restart docker
$ docker run -d --name redis-userns redis
$ cat /proc/$(docker inspect -f '{{ .State.Pid }}' redis-userns)/uid_map
         0       1000          1
         1     100000      65535

那么图形界面怎么办?声音怎么办呢?来我们看一下Firefox的例子。

首先看看Dockerfile:

FROM ubuntu
MAINTAINER Jean-Tiare Le Bigot <jt AT yadutaf DOT fr>

# Get PulseAudio for the sound, Firefox for, well, you know...
RUN apt-get update && apt-get -y install firefox pulseaudio

ENTRYPOINT ["firefox"]

Build且运行:

$ docker build -t firefox .
$ docker run --rm -it \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    -v /run/user/$UID/pulse/native:/run/pulse \
    -e DISPLAY=unix$DISPLAY \
    -e PULSE_SERVER=unix:/run/pulse \
    --name firefox \
    firefox --new-instance "https://www.youtube.com/watch?v=k1-TrAvp_xs"

它做了什么?

  • 分享了X11 socket

  • 将用户的pulseaudio socket分享成root的

  • 通过环境变量公开他们

  • 运行


作为(所希望的)副作用,使用用户命名空间设置docker守护程序可以有效地禁用各种安全敏感的选项,如启动特权容器或共享主机网络。这种额外的安全性来自内核的实现,我们当然不会拒绝它!

当然,这有局限性。 例如,如果您尝试使用chrome,您会因为发现没有声音而感到失望。这是因为chrome需要旧的Alsa音频系统,而且只能通过“audio”组访问。但是这个组目前还没有,也不能在Docker中映射。 但是linux内核支持这一点,而不是Docker。顺便说一句,如果你想测试chrome,请确保添加–disable-setuid-sandbox选项。

除了这个限制意外,其他部分是相当有趣的。使用类似的设置,您可以在主机上安装docker,充分利用其中的大部分功能,但是不会破坏您电脑的安全性或完整性。

赞 81