Docker之文件权限问题

前言

在非 root 权限使用 Docker 挂载数据卷的时候,产生出来的文件皆会是 root 权限,会出现无法写入的权限的问题


挂载时要注意

文件夹挂载

  • 宿主机 文件夹不存在空文件夹 挂载进容器,容器中对应的文件夹 将被清空
  • 宿主机 非空文件夹 挂载进容器,将会 覆盖 容器中原有文件夹

文件挂载

  • 禁止将不存在的文件挂载进容器中已经存在的文件上
  • 宿主机 存在的文件 挂载进容器,将会 覆盖 容器中对应的文件,若文件不存在则新建

正文

这里通过遇到的问题来理解 Docker 容器用户 UID 的使用,以及了解容器内外 UID 的映射关系,教大家用三种方式来设定容器使用者权限,以解决上述遇到的问题

问题

如果在 Docker 启动时,没有指定用户运行,容器会默认以 ROOT 进行启动。如果在容器内对挂载目录下的文件进行了操作,则相应文件的所有者就会升级为root,但在容器外如果只有非root用户的权限,就无法对这些文件进行操作了

比如在容器中创建 111 文件

1
2
3
4
5
pi@pi:~$ mkdir data
pi@pi:~$ ls -l
total 4
drwxrwxr-x 2 pi pi 4096 Feb 2 05:31 data
pi@pi:~$ sudo docker run -it -v $PWD/data:/test alpine /bin/sh -c 'touch /test/111'

看文件权限,可以看到文件是 root 权限

1
2
3
4
5
pi@pi:~$ tree -pugf data
[drwxrwxr-x pi pi ] data
└── [-rw-r--r-- root root ] data/111 # 文件是 root 权限

0 directories, 1 file

而我们当前的用户是 pi 用户

1
2
$ whoami
pi

当写入文件时,会因为权限问题被拒绝

1
2
pi@pi:~$ echo 'ok' > data/111
-bash: data/111: Permission denied

情况2

再比如,容器里的 mysql 用户的 uid2000,而宿主的 mysql 用户 uid1000,即便宿主要挂载的目录权限是 mysql:mysql,容器里看到的权限也是 1000:1000 ,会因权限被拒绝

情况3

还有一种情况,宿主不存在被挂载的目录,

1
pi@pi:~$ sudo docker run -it -v $PWD/data2:/test alpine /bin/sh -c 'touch /test/111'

Docker会以root权限先创建该目录再挂载

1
2
3
4
pi@pi:~$ ls -l
total 8
drwxrwxr-x 2 pi pi 4096 Feb 2 05:31 data
drwxr-xr-x 2 root root 4096 Feb 2 05:32 data2 # 当目录不存在,默认以 root 权限创建

这就导致以普通用户运行的容器进程无权限访问该目录

容器共享宿主机的UID

首先了解 UIDGID 的实现


Linux 内核负责管理 UIDGID,并通过内核级别的系统调用来决定是否通过请求的权限

l067dzkq.png

比如,当一个进程尝试去写文件,内核会检查创建这个进程的的用户的 UIDGID,来决定这个进程是否有权限修改这个文件,这里不是使用 username(用户名),而是 UID

当 Docker 容器运行在宿主机上的时候,仍然只有一个内核,容器共享宿主机的内核,所以所有的 UIDGID 都受同一个内核来控制

为什么容器里的用户名和宿主不一样

比如,superset 容器的用户叫做 superset,而本机没有 superset 这个用户,这是因为 username 不是 Linux Kernel 的一部分

简单的来说,username 是对 uid 的一个映射,**权限控制的依据是 uid,而不是 username**,验证权限时只认 UIDGID,所以不管用户名是什么,对一个特定文件的所有者,容器内外都是只认相应的 UID

解决

使用 Docker 指定使用者

可以通过 -u 将使用者 uid 及群组 gid 传入容器内

1
docker run -it -v $PWD/data:/test -u 当前uid:当前gid debian /bin/bash

查看当前使用者 uid 和 `gid

1
2
$ id
uid=1000(pi) gid=1000(pi) groups=1000(pi),10(wheel),996(docker)

为了更加方便,上述指令可以改成

1
docker run -it -v $PWD/data:/test -u $(id -u):$(id -g) debian /bin/bash -c 'touch /test/222'

查看权限

1
2
3
4
$ ls -l data/
total 16
-rw-r--r--. 1 root root 1 Feb 28 22:52 111
-rw-r--r--. 1 pi pi 0 Feb 28 23:00 222 # 是本机用户 pi 权限

使用 Dockerfile 指定使用者

也可以在 Dockerfile 内直接指定使用者

1
USER 1000:1000

但不推荐该方式,除非是在 容器 内独立建立使用者,并且指定权限

通过 docker-compose 指定使用者

通过 docker-compose 可以用 user 指定使用者权限来进行挂载

1
2
3
4
5
6
services:
agent:
image: xxxxxxxx
...
user: ${CURRENT_UID}
...

接着可以通过 .env 文件来指定变量的值

1
CURRENT_UID=1001:1001

通过用户空间

通过 user namespace 技术,把宿主机中的一个普通用户(只有普通权限的用户)映射到容器中的 root 用户

在容器中,该用户在自己的 user namespace 中认为自己就是 root,也具有 root 的各种权限,但是对于宿主机上的资源,它只有很有限的访问权限(普通用户)

步骤一

Ubuntu 不需要该步骤

Kernel 内核开启 namespace

1
2
grubby --args="namespace.unpriv_enable=1 user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"
echo "user.max_user_namespaces=15076" >> /etc/sysctl.conf

需要重启系统生效

1
reboot

步骤二

新建用户,这里的用户名为 2bacc

1
2
groupadd -g 500000 2bacc
useradd -u 500000 -g 2bacc 2bacc

/etc/subuid/etc/subgid 文件中都修改自动生成的从属ID范围

1
2
echo '2bacc:500000:65536' >>/etc/subuid
echo '2bacc:500000:65536' >>/etc/subgid

文件格式是

1
USERNAME:UID:RANGE
注释
USERNAME 映射到容器内的用户名
UID 为用户分配的初始 UID
RANGE 为用户分配的 UID 范围

用户 2bacc 在当前的 user namespace 中具有 65536 个从属用户,用户 ID 为 500000-565535,在一个子 user namespace 中,这些从属用户被映射成 ID 为 0-65535 的用户。subgid 的含义和 subuid 相同

步骤三

修改 /etc/docker/daemon.json 配置,新增 "userns-remap": "default" 选项,改好如下

1
2
3
4
5
6
7
$ cat /etc/docker/daemon.json 
{
"registry-mirrors": [
"https://ustc-edu-cn.mirror.aliyuncs.com"
],
"userns-remap": "2bacc"
}

区别

  • "userns-remap": "2bacc" 指定映射用户,演示用
  • "userns-remap": "default" 自动配置映射,实际用

注意:修改此项配置需要慎重,如果是已经部署了一套docker环境,启用此选项后,会切换到隔离环境,以前的docker容器将无法使用!

然后重启服务

1
systemctl restart docker

接下来我们发现在 /var/lib/docker 目录下新建了一个目录 500000.500000 也就是我们之前指定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ls -l /var/lib/docker
total 48
drwx--x--- 12 root 165536 4096 Feb 6 02:02 500000.500000
drwx--x--x 4 root root 4096 Feb 6 01:48 buildkit
drwx--x--- 2 root root 4096 Feb 6 01:48 containers
-rw------- 1 root root 36 Feb 6 01:48 engine-id
drwx------ 3 root root 4096 Feb 6 01:48 image
drwxr-x--- 3 root root 4096 Feb 6 01:48 network
drwx--x--- 3 root root 4096 Feb 6 01:57 overlay2
drwx------ 4 root root 4096 Feb 6 01:48 plugins
drwx------ 2 root root 4096 Feb 6 01:57 runtimes
drwx------ 2 root root 4096 Feb 6 01:48 swarm
drwx------ 2 root root 4096 Feb 6 01:57 tmp
drwx-----x 2 root root 4096 Feb 6 01:57 volumes

目录下的内容与 /var/lib/docker 基本一致,说明启用用户隔离后文件相关的内容都会放在新建的 500000.500000 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ls -l /var/lib/docker/500000.500000/
total 44
drwx--x--x 4 root root 4096 Feb 6 02:02 buildkit
drwx--x--- 2 root 165536 4096 Feb 6 02:09 containers
-rw------- 1 root root 36 Feb 6 02:02 engine-id
drwx------ 3 root root 4096 Feb 6 02:02 image
drwxr-x--- 3 root root 4096 Feb 6 02:02 network
drwx--x--- 5 root 165536 4096 Feb 6 02:09 overlay2
drwx------ 4 root root 4096 Feb 6 02:02 plugins
drwx------ 2 root root 4096 Feb 6 02:02 runtimes
drwx------ 2 root root 4096 Feb 6 02:02 swarm
drwx------ 2 root root 4096 Feb 6 02:08 tmp
drwx-----x 2 root root 4096 Feb 6 02:02 volumes

[2023-02-06T02:15:30.png]

测试

在容器里面创建一个 UID1000test 用户,他能读取挂载在容器里面的什么权限的文件呢?


我们先将

在宿主系统的 /data/test 目录上,创建几个不同的测试文件

1
mkdir -p /data/test && cd $_

一个属主和属组都为 501000 的文件,权限为600,文件名为:501000.txt

1
2
3
echo '1111' > 501000.txt \
&& chown 501000:501000 501000.txt \
&& chmod 600 501000.txt

一个属主和属组都为 2bacc(UID为500000)的文件,权限为600,文件名为:2bacc-root.txt

1
2
3
echo '2222' > 2bacc-root.txt \
&& chown 2bacc:2bacc 2bacc-root.txt \
&& chmod 600 2bacc-root.txt

一个在系统上权限为 root(UID为0)的文件,权限为600,文件名为:root.txt

1
2
echo '3333' > root.txt \
&& chmod 600 root.txt

一个在系统上权限为 root(UID为0)的文件,权限为666,文件名为:test.txt

1
2
echo '4444' > test.txt \
&& chmod 666 test.txt

查看下

1
2
3
4
5
6
$ ls -l
total 16
-rw------- 1 2bacc 2bacc 5 Feb 6 02:05 2bacc-root.txt
-rw------- 1 501000 501000 5 Feb 6 02:05 501000.txt
-rw------- 1 root root 5 Feb 6 02:05 root.txt
-rw-rw-rw- 1 root root 5 Feb 6 02:05 test.txt

启动一个的容器,并在里面创建一个 UID 为 1000test 用户,进入挂载的 /test 目录

1
2
docker run --rm -it -v /data/test:/test centos bash
useradd -u 1000 test && su - test

先在容器内执行 sleep 1000,然后在宿主机查看下进程所有者,已经恢复为 root

1
2
$ ps -elf | grep -v grep | grep sleep
4 S 2bacc 2977 2940 0 80 0 - 5762 hrtime 02:49 pts/0 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 1000

查看权限如下

1
2
3
4
5
6
7
8
9
[test@d8d39b0128bc ~]$ id
uid=1000(test) gid=1000(test) groups=1000(test)
[test@d8d39b0128bc ~]$ cd /test/
[test@d8d39b0128bc test]$ ls -l
total 16
-rw------- 1 root root 5 Feb 6 02:05 2bacc-root.txt
-rw------- 1 test test 5 Feb 6 02:05 501000.txt
-rw------- 1 nobody nobody 5 Feb 6 02:05 root.txt
-rw-rw-rw- 1 nobody nobody 5 Feb 6 02:05 test.txt

结果是只有 501000.txttest.txt 这两个文件可以看到里面内容,剩下两个均没有权限

1
2
3
4
5
6
7
8
[test@d8d39b0128bc test]$ cat 2bacc-root.txt 
cat: 2bacc-root.txt: Permission denied
[test@d8d39b0128bc test]$ cat 501000.txt
1111
[test@d8d39b0128bc test]$ cat root.txt
cat: root.txt: Permission denied
[test@d8d39b0128bc test]$ cat test.txt
4444

其中 501000.txt 有权限,是因为在系统上这个文件的属主和属组都是 501000,其映射在容器里面的属主和属组就是 1000,所以 UID1000 的 test 用户可以查看里面的内容

1
2
$ ls -l 501000.txt 
-rw------- 1 501000 501000 5 Feb 6 02:05 501000.txt

test.txt 能查看,因为文件的权限是 666other 位的权限是 rw 所以其他用户都有权限查看

1
2
$  ls -l test.txt 
-rw-rw-rw- 1 root root 5 Feb 6 02:05 test.txt

如果要给容器里面挂载一个有 root 权限的目录,在 宿主 上就需要给这个目录权限给 2bacc

比如要挂载的目录是 /data/test,那命令就是命令如下

1
chown -R 2bacc:2bacc /data/test

然后再容器里查看下,就都是 root 权限了

1
2
3
4
5
6
7
8
[test@d8d39b0128bc test]$ ls -al
total 24
drwxr-xr-x 2 root root 4096 Feb 6 02:05 .
drwxr-xr-x 1 root root 4096 Feb 6 02:38 ..
-rw------- 1 root root 5 Feb 6 02:05 2bacc-root.txt
-rw------- 1 root root 5 Feb 6 02:05 501000.txt
-rw------- 1 root root 5 Feb 6 02:05 root.txt
-rw-rw-rw- 1 root root 5 Feb 6 02:05 test.txt

临时禁用

一旦设置了 "userns-remap" 参数,所有的容器默认都会启用用户隔离的功能,有些情况下我们可能需要回到没有开启用户隔离的场景

可以通过 --userns=host 为单个的容器禁用用户隔离功能。或在 version: "3" 版本的 docker-compose.yml 文件中对应的容器增加 userns_mode: "host"


以同样的 2bacc 1000 为例子,启动的时候加上 --userns=host

1
2
docker run --rm -it --userns=host -v /data/test:/test centos bash
useradd -u 1000 test && su - test

先在容器内执行 sleep 1000,然后在宿主机查看下进程所有者,已经恢复为 root

1
2
$ ps -elf | grep -v grep | grep sleep
4 S root 2822 2751 0 80 0 - 5762 hrtime 02:48 pts/0 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 1000

要是有点晕就自己操作一遍,啥都会明白…