无论是 docker 启动一个 container 还是在 k8s 中 deploy 一个 Pod 都可以指定 privileged 参数,之前在 Pod 的 spec YAML file 也里曾经用过,但是一直没有仔细想过加上这个参数后有什么不一样,今天就来研究一下。

首先来看一个最直观的对比,先运行一个没有 privileged 的容器:

$ docker run --rm -it ubuntu:18.04 bash

root@e6f5f42c5b7e:/# ls /dev/
console  core  fd  full  mqueue  null  ptmx  pts  

root@e6f5f42c5b7e:/# fdisk -l

再来看看如果加上了 privileged 会有什么不一样:

$ docker run --rm -it --privileged ubuntu:18.04 bash

root@8e28f79eec9e:/# ls /dev/
tty11  tty2   tty28  tty36  tty44  tty52  tty60  
...
...
root@8e28f79eec9e:/# fdisk -l
Disk /dev/loop0: 97.1 MiB, 101777408 bytes, 198784 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

不止能看到设备文件,甚至还能 mount 宿主机的文件系统,

root@8e28f79eec9e:~# mount /dev/sda2 test/
root@8e28f79eec9e:~# cd test/

root@8e28f79eec9e:~/test# cd home/yourname/
root@8e28f79eec9e:~/test/home/yourname# echo "hello,world" >> testfile

yourname@host:~$ cat testfile
hello,world

可见 privileged 模式的权限还是很大的,容器能直接修改 host 上的文件。

在不加 privileged 参数时,虽然进入容器后看到的也是 root 用户,但是这是因为容器内默认使用了 user ID 为 0,而 0 表示的就是 root,所以这只是一个名字显示的问题,而不代表用户真的有了 root 权限。

我们可以在容器启动时加入 --user 参数来设置想要的 user ID,比如

$ docker run --rm --user 4000 -it ubuntu:18.04 bash
I have no name!@b0349ec73c3a:/$ ls

这样我们就看到了常见的 “I have no name!"。

关于容器的 UID 和 GID 问题,在另外一篇博客中再详细研究,这里就不展开了。

docker in docker

接着思考,既然都能把 /dev/sda2 这样的设备都挂载了,那是不是也能找到 /var/run/docker.sock 文件呢? 这样我就可以在容器内用 docker 命令操作 Host 上的其他容器了。

于是操作一番

root@7463d90ab1eb:~# mkdir test
root@7463d90ab1eb:~# mount /dev/sda2 test/

root@7463d90ab1eb:~/test/var# ls -l
lrwxrwxrwx  1 root root     4 Feb  3  2020 run -> /run

root@7463d90ab1eb:~/test/run# ls
dnsmasq  grub  initctl  lock  log  lvm  mount  screen  
sendsigs.omit.d  shm  sudo  systemd  user  utmp

很奇怪,不单单是 docker.sock 文件不在其中,Host 上 /run 中的很多其他文件也不在,因此无发达成目标。

后来经过一番搜索资料,还是没搞明白为啥找不到 sock 文件,先暂且放一下,有空再研究

既然自创的方法不行,那就用一下 docker 官方的方法了。官方提供了一个名叫 docker:dind 的镜像,也就是 docker-in-docker 的缩写。

运行该镜像也需要 privileged 权限,并且只能以 -d daemon 的方式运行

$ docker run -d --name dind --privileged docker:dind # 启动容器
$ docker logs -f dind # 查看启动日志
$ docker exec -it dind sh # 进入容器

之后就可以在容器内部执行 docker 命令了。

这种方式的优点是完全独立的一个docker,与 Host 是隔离的,在上面做任何操作都不会影响 Host 上的 docker,当 dind 容器销毁后,里面容器和镜像也都没有了。另外,容器文件系统 AUFS 不支持这种嵌套,因此 dind 的容器用的是 overlay2 类型的文件系统。

dind 的原理

注意,虽然 dind 方式也是开启了 privileged,但它并不是用上一节开头我想的直接挂载 /var/run/docker.sock,两者是不一样的。

参考 wrapdocker 源码,大概思路是挂载 cgroup, tmpfs, cgroup 的SUBSYS, 关掉不需要的文件描述符,启动dockerd

容器逃逸

说到这里,那就再来说一句容器逃逸。容器逃逸有很多种,有的是 docker 运行时的 vulnerability,有的是因为 Linux 内核的 bug,前两种都太难了,这里只说一种最简单的——配置不当,最典型的例子就是挂载 /var/run/docker.sock 文件到容器内。

(Host)# docker run -it -v /var/run/:/host/var/run/ ubuntu:18.04 /bin/bash

# apt-get install docker.io

# docker -H unix:///host/var/run/docker.sock  container ls 

最后一条命令可以显示出 Host 主机上正在运行的容器。

参考资料