本文发自 http://www.binss.me/blog/solve-docker-permission-problem-by-using-user-namespace/,转载请注明出处。

最近买了一台 NUC 作为新玩具,于是尝试在上面部署各种服务,其中一个用途就是作为 NAS。

为了方便部署和监控,我默默的装上了 Docker (真香),装上 18.05.0-ce 后发现以前的一些小毛病都解决得差不多了,遂开始部署各种各样的服务。

没想到在此过程中还是踩坑了,以文记之。

需求和问题

实现离线下载 + 文件统一管理。

有两个容器,aria2 挂载宿主机 downloads 目录,负责将文件下载到该目录。nextcloud 作为个人云盘,同样挂载宿主机 downloads 目录,并通过外部存储的方式加入管理。

结果发现要么 nextcloud 无法挂载 downloads / 挂载后没文件 / 挂载后文件不可删除,要么 aria2 无法写入到 downloads 目录。

本质的问题是用户的权限没配好。这需要从 docker 的实现说起。

Docker User Privilege

默认情况下,docker 以 root 权限运行 container 中的进程,于是当你在宿主机通过 ps 进行观察时,你会发现这些进程的 user 为 root 。也就是说容器里的用户和宿主机的用户是一一对应的,容器里的 root 就是相宿主机的 root ,这不是很安全,因为在挂载宿主机目录的情况下,容器进程能够以 root 权限修改这个目录。

为了解决这个问题,很多 container 在 Dockerfile 中会新建一个普通用户,然后在 entrypoint 或程序中切换到这个用户,比如 nginx 会根据配置文件中的 user 来切,nextcloud 中的 php-fpm pool 会切换到 www-data 用户,redis 会切换到 redis 用户......

这样做安全是安全了:在宿主机看来,容器中的进程以非 root 用户运行。但有一个问题依然存在:如果容器挂载了一个宿主机目录,然后容器中的进程在这个目录中创建文件,则这个文件的 owner 就是容器进程的用户,无论是 root 还是 www-data ,运行该宿主机用户都没办法访问该文件。

仔细想想,这样的模型是你想要的吗?

我觉得更自然的关系应该是这样:容器里的所有用户,权限不得高于运行容器的用户。比如说宿主机 binss 用户创建了一个容器,那个容器里的 root 用户对应的是宿主机的 binss,容器里的非 root 拥有的权限应该低于 binss ,binss 对于其他容器中用户而言就是 root,自然也就能够访问他们创建的所有文件。

要建立这种映射关系,我们很自然的想到了 Linux 中的 user namespace 。

Linux User Namespace

User namespace 是 Linux 2.6.23 引入的 namespace,在 3.8 中趋于完善。目的是建立在父 namespace 和 子 namespace 之间建立用户的映射关系。

作为子 user namespace 的创建者,父 user namespace 的普通用户可以在子 user namespace 中成为 root ,同时拥有子 user namespace 及其所有子孙的所有 capabilities。

创建 user namespace 很简单,只需在 clone 产生子进程时指定 CLONE_NEWUSER flag,这样子进程就会运行在子 user namespace 中。

Docker User Namespace

那么在 docker 中能不能使用 user namespace 呢?实际上,docker 直到 Docker Engine 1.10(2016-02-04) 才实现了这个功能,称之为 userns-remap 。

要开启的话,需要编辑 /etc/docker/daemon.json ,添加以下内容:

{
  "userns-remap": "binss"
}

其中参数的内容为要将容器中 root 映射到的宿主机用户和用户组。如果指定为 default,则 docker 会自动创建并映射为 dockremap 用户和用户组。

对于我来说,我希望将容器中的 root 用户映射为我自己的用户:binss,用户组映射为我自己的用户组:binss 。

随后还需要修改 /etc/subuid/etc/subgid,建立宿主机用户 / 用户组到容器用户的映射:

binss:1000:1
binss:100000:65536

其中 1000 为宿主机中 binss 的 uid,这表示将容器中的 root 用户映射到宿主机 uid 为 1000 的用户(binss);对于其他普通用户,映射到 uid 100000 到 100000 + 65536 之间的用户,并同样受到 binss 用户的管理。

gid 同理:

binss:1000:1
binss:100000:65536

建立完成后,重启 docker :

sudo systemctl restart docker

重启后,发现在 /var/lib/docker 目录下多一个名为 1000:1000 的文件夹,其目录结果和 /var/lib/docker 一摸一样。这时候再看看你的容器和镜像,发现都为空,这说明开启 userns-remap 后 docker 会使用全新的环境。如果想要切回原来的环境,只需删掉 ~/.docker/config.json 中相应的内容即可。

说回之前我遇到的问题:nextcloud 容器中 php-fpm 进程的用户为 www-data ,aria2 容器中 aria2 进程的用户为 root,于是通过 aria2 下载的文件,owner 为 root,这也就是 nextcloud 挂载后无法正常访问的原因。

此外,如果将 nextcloud 的配置文件 config.php 以及主数据目录也通过挂载的方式进行挂载的话,php-fpm 进程将无法访问,官方文档给出的方案是 chown 成 www-data:www-data 。这样一来,宿主机用户 binss 就没有办法直接读写这些文件了。

在开启了 userns-remap 后,问题就好办多了。

容器中的 root 进程在宿主机中被映射成 binss 用户的进程,而非 root 进程被映射为 uid 100000+ 用户的进程。因此当前的宿主机用户 binss 能够读写容器的任何文件,比如 aria2 容器中以 root 用户下载下来的文件,在宿主机看来 owner 就是 binss 。

但在 nextcloud 中的 php-fpm 却没有办法访问宿主机中 owner 为 binss 的文件,因为它是以 www-data 用户来运行的,没有办法读写对于它来说是 root 的文件。因此我的解决办法是让 php-fpm 以 root 的身份运行,比较坑的是直接修改 php-fpm 的配置文件 /usr/local/etc/php-fpm.d/www.conf 运行时还会报错:

ERROR: [pool www] please specify user and group other than root

google 一番得知需要在运行 php-fpm 时加上 -R 参数。因此 Dockerfile 变成这样:

FROM nextcloud:fpm

COPY www.conf /usr/local/etc/php-fpm.d/www.conf
COPY redis.config.php /usr/src/nextcloud/config/redis.config.php

CMD ["php-fpm", "-R"]

别忘了,php-fpm 虽然在容器中是 root,但在宿主机中只是一个普通用户(binss),保证了安全性。

参考

Use Linux user namespaces to fix permissions in docker volumes

Isolate containers with a user namespace