Docker之文件权限问题
前言
在非 root
权限使用 Docker 挂载数据卷的时候,产生出来的文件皆会是 root
权限,会出现无法写入的权限的问题
挂载时要注意
文件夹挂载
- 宿主机
文件夹不存在
或空文件夹
挂载进容器,容器中对应的文件夹将被清空
- 宿主机
非空文件夹
挂载进容器,将会覆盖
容器中原有文件夹
文件挂载
- 禁止将不存在的文件挂载进容器中已经存在的文件上
- 宿主机
存在的文件
挂载进容器,将会覆盖
容器中对应的文件,若文件不存在则新建
正文
这里通过遇到的问题来理解 Docker
容器用户 UID
的使用,以及了解容器内外 UID
的映射关系,教大家用三种方式来设定容器使用者权限,以解决上述遇到的问题
问题
如果在 Docker 启动时,没有指定用户运行,容器会默认以 ROOT
进行启动。如果在容器内对挂载目录下的文件进行了操作,则相应文件的所有者就会升级为root,但在容器外如果只有非root用户的权限,就无法对这些文件进行操作了
比如在容器中创建 111
文件
1 | pi@pi:~$ mkdir data |
看文件权限,可以看到文件是 root
权限
1 | pi@pi:~$ tree -pugf data |
而我们当前的用户是 pi
用户
1 | $ whoami |
当写入文件时,会因为权限问题被拒绝
1 | pi@pi:~$ echo 'ok' > data/111 |
情况2
再比如,容器里的 mysql
用户的 uid
是 2000
,而宿主的 mysql
用户 uid
是 1000
,即便宿主要挂载的目录权限是 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 | pi@pi:~$ ls -l |
这就导致以普通用户运行的容器进程无权限访问该目录
容器共享宿主机的UID
首先了解 UID
、GID
的实现
Linux 内核负责管理 UID
和 GID
,并通过内核级别的系统调用来决定是否通过请求的权限
比如,当一个进程尝试去写文件,内核会检查创建这个进程的的用户的 UID
和 GID
,来决定这个进程是否有权限修改这个文件,这里不是使用 username
(用户名),而是 UID
当 Docker 容器运行在宿主机上的时候,仍然只有一个内核,容器共享宿主机的内核,所以所有的 UID
和 GID
都受同一个内核来控制
为什么容器里的用户名和宿主不一样
比如,superset 容器的用户叫做 superset
,而本机没有 superset
这个用户,这是因为 username
不是 Linux Kernel 的一部分
简单的来说,username
是对 uid
的一个映射,**权限控制的依据是 uid
,而不是 username
**,验证权限时只认 UID
和 GID
,所以不管用户名是什么,对一个特定文件的所有者,容器内外都是只认相应的 UID
的
解决
使用 Docker 指定使用者
可以通过 -u
将使用者 uid 及群组 gid 传入容器内
1 | docker run -it -v $PWD/data:/test -u 当前uid:当前gid debian /bin/bash |
查看当前使用者 uid
和 `gid
1 | $ id |
为了更加方便,上述指令可以改成
1 | docker run -it -v $PWD/data:/test -u $(id -u):$(id -g) debian /bin/bash -c 'touch /test/222' |
查看权限
1 | $ ls -l data/ |
使用 Dockerfile 指定使用者
也可以在 Dockerfile 内直接指定使用者
1 | USER 1000:1000 |
但不推荐该方式,除非是在 容器 内独立建立使用者,并且指定权限
通过 docker-compose 指定使用者
通过 docker-compose 可以用 user
指定使用者权限来进行挂载
1 | services: |
接着可以通过 .env
文件来指定变量的值
1 | CURRENT_UID=1001:1001 |
通过用户空间
通过 user namespace
技术,把宿主机中的一个普通用户(只有普通权限的用户)映射到容器中的 root
用户
在容器中,该用户在自己的 user namespace
中认为自己就是 root
,也具有 root
的各种权限,但是对于宿主机上的资源,它只有很有限的访问权限(普通用户)
步骤一
Ubuntu 不需要该步骤
Kernel 内核开启 namespace
1 | grubby --args="namespace.unpriv_enable=1 user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)" |
需要重启系统生效
1 | reboot |
步骤二
新建用户,这里的用户名为 2bacc
1 | groupadd -g 500000 2bacc |
在 /etc/subuid
和 /etc/subgid
文件中都修改自动生成的从属ID范围
1 | echo '2bacc:500000:65536' >>/etc/subuid |
文件格式是
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 | $ cat /etc/docker/daemon.json |
区别
"userns-remap": "2bacc"
指定映射用户,演示用"userns-remap": "default"
自动配置映射,实际用
注意:修改此项配置需要慎重,如果是已经部署了一套docker环境,启用此选项后,会切换到隔离环境,以前的docker容器将无法使用!
然后重启服务
1 | systemctl restart docker |
接下来我们发现在 /var/lib/docker
目录下新建了一个目录 500000.500000
也就是我们之前指定的
1 | $ ls -l /var/lib/docker |
目录下的内容与 /var/lib/docker
基本一致,说明启用用户隔离后文件相关的内容都会放在新建的 500000.500000
目录下
1 | $ ls -l /var/lib/docker/500000.500000/ |
[]
测试
在容器里面创建一个 UID
为 1000
的 test
用户,他能读取挂载在容器里面的什么权限的文件呢?
我们先将
在宿主系统的 /data/test
目录上,创建几个不同的测试文件
1 | mkdir -p /data/test && cd $_ |
一个属主和属组都为 501000
的文件,权限为600,文件名为:501000.txt
1 | echo '1111' > 501000.txt \ |
一个属主和属组都为 2bacc
(UID为500000)的文件,权限为600,文件名为:2bacc-root.txt
1 | echo '2222' > 2bacc-root.txt \ |
一个在系统上权限为 root(UID为0)的文件,权限为600
,文件名为:root.txt
1 | echo '3333' > root.txt \ |
一个在系统上权限为 root(UID为0)的文件,权限为666
,文件名为:test.txt
1 | echo '4444' > test.txt \ |
查看下
1 | $ ls -l |
启动一个的容器,并在里面创建一个 UID 为 1000
的 test
用户,进入挂载的 /test
目录
1 | docker run --rm -it -v /data/test:/test centos bash |
先在容器内执行 sleep 1000
,然后在宿主机查看下进程所有者,已经恢复为 root
1 | $ ps -elf | grep -v grep | grep sleep |
查看权限如下
1 | [test@d8d39b0128bc ~]$ id |
结果是只有 501000.txt
和 test.txt
这两个文件可以看到里面内容,剩下两个均没有权限
1 | [test@d8d39b0128bc test]$ cat 2bacc-root.txt |
其中 501000.txt
有权限,是因为在系统上这个文件的属主和属组都是 501000
,其映射在容器里面的属主和属组就是 1000
,所以 UID
为 1000
的 test 用户可以查看里面的内容
1 | $ ls -l 501000.txt |
而 test.txt
能查看,因为文件的权限是 666
,other
位的权限是 rw
所以其他用户都有权限查看
1 | $ ls -l test.txt |
如果要给容器里面挂载一个有 root
权限的目录,在 宿主
上就需要给这个目录权限给 2bacc
比如要挂载的目录是 /data/test
,那命令就是命令如下
1 | chown -R 2bacc:2bacc /data/test |
然后再容器里查看下,就都是 root
权限了
1 | [test@d8d39b0128bc test]$ ls -al |
临时禁用
一旦设置了 "userns-remap"
参数,所有的容器默认都会启用用户隔离的功能,有些情况下我们可能需要回到没有开启用户隔离的场景
可以通过 --userns=host
为单个的容器禁用用户隔离功能。或在 version: "3"
版本的 docker-compose.yml
文件中对应的容器增加 userns_mode: "host"
以同样的 2bacc 1000
为例子,启动的时候加上 --userns=host
1 | docker run --rm -it --userns=host -v /data/test:/test centos bash |
先在容器内执行 sleep 1000
,然后在宿主机查看下进程所有者,已经恢复为 root
1 | $ ps -elf | grep -v grep | grep sleep |
要是有点晕就自己操作一遍,啥都会明白…