在docker挂载磁盘的时候,由于很多容器内使用root运行程序,会导致挂载中产生的文件属于root:root。一般容器外用户并不是root,会让文件共享甚至阅读日志产生不必要的麻烦。本文旨在不更改容器的情况下,从根本上解决产生root权限文件的问题。

docker产生的文件经常需要sudo chmod o+rw *然后访问,虽然麻烦,但好歹有sudo的权限。没有sudo权限的时候就没那么走运了,这时候我发现了三种操作可以解决这个问题。

文章分为三部分,解释了挂载的原理、提供了三种解决原理、就方案进行演示。若仅希望获得推荐的解决方案,可以直接跳到方案演示的使用子用户一节。

文件挂载原理

简而言之,docker的文件挂载中,容器内外uid与gid相同

最简单的挂载磁盘,例如:docker run -v $PWD:/data bash,就是将当前目录挂载到容器内的/data目录。文件的权限可以通过ls -l查看。

容器内可能有使用root用户和非root用户运行程序的两种情况。1)若容器内使用root运行程序,可以想象,产生的文件属于0:0,也就是root:root。容器外文件也会属于0:0,同样是root:root。这样产生的文件就需要容器外的root权限才可以操作。2)若容器内使用非root运行程序,例如blog:x:1000:1000::/home/blog:/bin/bash,产生的文件属于1000:1000。容器外文件也会属于1000:1000,而容器外uid为1000的用户可能是frank,这是若碰巧你就是frank,你就可以顺利读取这个文件。但若你是uid为1001的david,而且frank是个讨厌鬼,你就告别这个文件了。甚至容器外根本没有uid为1000的用户,那这个文件就不属于任何人。

三种解决原理

限定开启用户

部分容器可以使用该方案,且难以判断是否可行。对于这些容易配置权限的容器,这个问题很容易解决,强制容器的运行用户docker run -u blog bash即可。

我们这么定义容易配置权限的容器。

  1. 容器内所有用户都可以访问容器内所有需要访问、运行的资源

  2. 容器运行时由运行用户生成所有文件

  3. 由运行用户访问挂载磁盘内需要访问、运行的资源

满足这些要求,会让配置一个稍复杂容器的工作变得复杂许多甚至不可完成。如果你创建过容器,那你一定可以想象。第一点起码可以通过批量更改所有文件的o属性完成。第二点和第三点,容器运行的程序若需要绑定保留端口,就需要特别的各种设置;容器运行的程序若简单的要求需要管理员权限,我们不可能为了创建符合要求的docker更改甚至反编译程序。

而我们大部分时间使用到的容器都是他人配置好的,我们无法判断他人配置的容器是否属于容易配置权限的容器。这种容器若强制容器的运行用户除了无法保证产生文件的权限,还可能让容器意外崩溃。但说不准,bug就莫名奇妙消失了?

使用子用户

若容器内外文件的uid:gid可以不同,这个问题就可以很容易解决。的确有这么一个方案,让容器内所有的uid:gid以一定的规则映射,我推荐使用这一方案。文章的方案演示节中我会演示这一方案。

在官方文档中,这个方案本身是为了解决安全问题,见这里。发生容器挂载磁盘root权限提权也不是一次两次了,这算是一个官方的解决方案。

但这个方案会有一些限制

  • 不可使用--pid=host--network=host
  • 不可使用无法识别或使用用户映射的挂载磁盘
  • 若使用docker run --privileged则必须同时使用--userns=host

都是不大的限制,使用的时候留个心眼即可。

osxfs

有趣的是,OSX中不存在这一问题。

在和朋友讨论的时候,朋友们纷纷反映没有这个问题,不禁让我感叹贫穷限制了我的想象力。如果你使用的是OSX或者和我一样从朋友那里抢来了一台OSX,你可以尝试如下命令:

1
2
3
4
5
6
7
8
mkdir data
docker run -it --rm \
-v $PWD/data:/data \
bash:5.0 \
touch /data/testfile
ls -l data

# -rw-r--r-- 1 user staff 0 0 0 0:00 testfile

尽管OSX版本的docker有各种问题,但起码没有这个问题。这一切要归功于osxfs,官方文档中有一个基本的介绍

其中Ownership一段提到了解决方案,容器内root映射到容器外运行docker的用户,容器内更改文件所有人的记录保存在com.docker.owner中。具体细节还有一些别的,TL;DR。

Initially, any containerized process that requests ownership metadata of an object is told that its uid and gid own the object. When any containerized process changes the ownership of a shared file system object, such as by using the chown command, the new ownership information is persisted in the com.docker.owner extended attribute of the object. Subsequent requests for ownership metadata return the previously set values. Ownership-based permissions are only enforced at the macOS file system level with all accessing processes behaving as the user running Docker. If the user does not have permission to read extended attributes on an object (such as when that object’s permissions are 0000), osxfs attempts to add an access control list (ACL) entry that allows the user to read and write extended attributes. If this attempt fails, the object appears to be owned by the process accessing it until the extended attribute is readable again.

所以内容被存储到了这里:

1
2
3
4
5
6
7
8
docker run -it --rm \
-v $PWD/data:/data \
bash:5.0 \
chown nobody /data/testfile
ls -l@ data

# -rw-r--r--@ 1 user staff 0 0 0 0:00 testfile
# com.docker.owner 9

所以这个方案无法应用在其他系统,而一般OSX上的docker也就是个测试。

一些其他的想法

如果使用特定用户构建容器,最后也是这个用户使用容器,就没有这个问题了。比如构建时容器内用户uid为1000,使用容器的用户uid也为1000。不过这样的容器基本不可移植,但自用时也不失为一个简单的办法。

另一个想法是,是否有用户的权限低于所有用户。就像root用户可以访问所有用户文件类似,若有一个用户的文件可以被所有用户访问,那使用这个用户创建docker就解决了这个问题。但实际上找不到这样一个用户,即使是nobody也不行,只有没有人可以访问没有人的文件。

方案演示

限定开启用户

容器内强制由容器外的当前用户运行容器内的程序。

1
2
3
4
5
docker run -it --rm \
-u `id -u` \
-v $PWD/data:/data \
bash:5.0 \
touch /data/solution1

可以看到生成的文件由当前用户所有。

1
2
3
ls -l data

# -rw-r--r-- 1 blog blog 0 0 00:00 solution1

该方案最为简单,但正如原理节讲到的,对于很多容器,强制容器的运行用户无法保证产生文件的权限,还可能让容器意外崩溃。

使用子用户

该方案需要更改docker守护程序的运行参数,测试的时候建议开个虚拟机,这里以一个未进行任何配置的Ubuntu 16为例进行展示。

先完成准备,安装docker并创建用户blog作为演示用户。

1
2
3
4
5
6
7
8
9
10
# install docker
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh

# add user blog
useradd -d /home/blog -m -s /bin/bash -G sudo,docker blog
passwd blog

# switch to blog
su blog
cd ~ && mkdir data

若不设置子用户,生成的文件将会归root所有。

1
2
3
4
5
6
7
docker run -it --rm \
-v $PWD/data:/data \
bash:5.0 \
touch /data/blog_without_subuid

ls -l data
# -rw-r--r-- 1 root root 0 0 00:00 blog_without_subuid

subuid的设置被放在/etc/subuid中,当前用户的uid可以通过id -u获取。subgid的设置被放在/etc/subgid中,当前用户的专属group可以通过cat /etc/group | grep $USER获取。这里假设blog:blog对应的uid:gid为1000:1000,那么这两个文件按照如下进行设置。子用户的格式是name:start:number,意思是名字为name的用户生成的子用户是从start开始的number个。这两行产生了容器外uid为1000, 100000, 100001, 100002...的一系列用户,和容器内的用户一一对应,容器内的uid为0, 1, 2, 3...。显而易见,容器内的root映射到了容器外的当前用户,子用户组也是同理。

1
2
3
4
5
6
7
# sudo vim /etc/subuid
blog:1000:1
blog:100000:65536

# sudo vim /etc/subgid
blog:1000:1
blog:100000:65536

下面通过设置docker的配置文件,让docker知道需要启动用户映射,然后重启docker即可。

1
2
3
4
5
6
# sudo vim /etc/docker/daemon.json
{
"userns-remap": "bloger"
}

# sudo service docker restart

到这一步,子用户的设置就完成了,可以试一试效果。

1
2
3
4
5
6
7
docker run -it --rm \
-v $PWD/data:/data \
bash:5.0 \
touch /data/blog_with_subuid

ls -l data
# -rw-r--r-- 1 blog blog 0 0 00:00 blog_with_subuid

虽然容器内使用root生成了归属于root的文件,但是容器外文件归属于设置了映射的当前用户。