前言
此篇博文是笔者所总结的 Docker 系列之一;
本文为作者的原创作品,转载需注明出处;
Linux Namespace 概述
Linux 内核从版本 2.4.19 开始陆续引入了 namespace 的概念;
Linux Namespace 目的
The purpose of each namespace is to wrap a particular global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. 它的目的是将某个特定的全局系统资源(global system resource)通过抽象方法使得在 namespace 中的进程看起来他们自己就是该全局资源的一个实例。
六中 Namespace 隔离措施
namespace | 内核版本引入 | 被隔离的全局资源 | 在容器下的隔离效果 |
---|---|---|---|
Mount namespaces | Linux 2.4.19 | 文件系统挂接点 | 每个容器能看到不同的文件系统层次结构 |
UTS namespaces | Linux 2.6.19 | nodename 和 domainname | 每个容器可以有自己的 hostname 和 domainame |
IPC namespaces | Linux 2.6.19 | 特定的进程间通信资源,包括 System V IPC 和 POSIX message queues | 每个容器有其自己的 System V IPC 和 POSIX 消息队列文件系统,因此,只有在同一个 IPC namespace 的进程之间才能互相通信 |
PID namespaces | Linux 2.6.24 | 进程 ID 数字空间 (process ID number space) | 每个 PID namespace 中的进程可以有其独立的 PID; 每个容器可以有其 PID 为 1 的root 进程;容器中的 PID 与 host 上的 PID 有一一映射的关系 |
Network namespaces | 始于Linux 2.6.24 完成于 Linux 2.6.29 | 网络相关的系统资源 | 每个容器用有其独立的网络设备,IP 地址,IP 路由表,/proc/net 目录,端口号等等。 |
User namespaces | 始于 Linux 2.6.23 完成于 Linux 3.8) | 用户和组 ID 空间 | 在 user namespace 中的进程的用户和组 ID 可以和在 host 上不同; |
Docker 中的 Namespace 使用
PID Namesampe
案例
创建一个镜像,默认让容器执行do echo 'hello world' | nc -l -p 8888
命令1
2
3
4FROM ubuntu:14.04
MAINTAINER comedshang <comedshang@unknow.com>
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done
使用如上Dockerfile
创建exposed_port
镜像,然后执行,容器命名为exposed_port_c
1 | mac@ubuntu:~/docker7$ docker run --name exposed_port_c exposed_port |
让我们来观察Host
与Container
之间的 PID 的映射的情况;
首先我们来观察
Container
的情况
进入正在执行的exposed_port Container
中的bash
环境,然后查看其当前进程的情况1
2
3
4
5
6
7mac@ubuntu:~/docker10$ docker exec -it exposed_port_c bash
root@729834d39b5e:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:20 ? 00:00:00 /bin/sh -c while true; do echo 'hello world' | nc -l -p 8888; done
root 6 1 0 04:20 ? 00:00:00 nc -l -p 8888
root 7 0 1 04:25 ? 00:00:00 bash
root 21 7 0 04:25 ? 00:00:00 ps -ef可以看到,容器中主要运行的有两个进程,
- PID 1, PPID 0
/bin/sh -c while true; do echo 'hello world' | nc -l -p 8888; done
这是容器的 init 进程,因为执行的是/bin/sh
,所以我们简称“容器 /bin/sh 进程” - PID 6, PPID 1
nc -l -p 8888
,我们简称“容器 nc 进程”
- PID 1, PPID 0
再次我们来观察
Host
的情况我们在 Host 中来找“容器 /bin/sh 进程”
1
2mac@ubuntu:~/docker7$ ps -ef | grep "/bin/sh -c while true; do echo 'hello world' | nc -l -p 8888; done"
root 11483 11468 0 12:20 ? 00:00:00 /bin/sh -c while true; do echo 'hello world' | nc -l -p 8888; done可以看到,
Host
上启动了一个 PID = 11483 的进程来执行“容器 /bin/sh 进程”,所以,这里我们可以看到 Host 与 Container 进程之间的映射关系是Host PID(11483) <-> Container PID(1)
;我们在 Host 中来找“容器 nc进程”
1
2mac@ubuntu:~/docker7$ ps -ef | grep "nc -l -p 8888$"
root 11514 11483 0 12:20 ? 00:00:00 nc -l -p 8888(注意,这里使用正则表达式
$
来避免重复搜索到“容器 /bin/sh 进程”。)
可以看到,Host
上启动了一个 PID = 11514 的进程来执行“容器nc
进程”,所以,Host 与 Container 之间的映射关系是Host PID(11514) <-> Container PID(6)
Docker 是如何实现的
- Docker Engine
管理镜像,并交由 Containerd 执行。 - Containerd
一个守护进程,通过调用 通过 Container-shim 和 runC 管理着容器的生命周期,开始、停止、暂停和销毁。由于容器启动以后(既 Containerd Deamon 启动以后),不需要依赖 Docker Engine,所以,Docker Engine 升级的时候,无需关闭当前正在执行的容器。 Containerd-shim
容器中的 init 进程在 Host 上所对应的进程的父进程就是 Containerd-shim
继续使用上述的用例,容器中的 init 进程对应 Host PID 11483, 其 PPID = 11468,我们查看 11486 是什么,1
2mac@ubuntu:~/docker7$ ps -ef | grep 11468
root 11468 20408 0 12:20 ? 00:00:00 docker-containerd-shim 729834d39b5e2a7f9336f3fb4ba63075c3d32d8bba65f07fd5b945043e5c333a /var/run/docker/libcontainerd/729834d39b5e2a7f9336f3fb4ba63075c3d32d8bba65f07fd5b945043e5c333a docker-runc果不其然,是
docker-containerd-shim
进程,看来每一个容器的 init 进程都由一个docker-containerd-shim
父进程管理其生命周期。runC
一个轻量级工具,就是用来运行容器的。
UTS Namespace
每个容器可以有自己的 hostname 和 domainame,且与 host 不同
Host
1
2mac@ubuntu:~/docker7$ hostname
ubuntuContainerd
1
2
3mac@ubuntu:~/docker7$ docker exec -it exposed_port_c bash
root@729834d39b5e:/# hostname
729834d39b5e
User Namespace
风险
如果基础镜像使用的是 root 用户构建,那么其 Docker 容器在执行过程中会继承基础镜像中的 root 用户执行;但需要特别注意的是,这里,容器执行所使用的 root 用户和 host 主机上的 root 用户,是同一个用户。那么,如果使用了Volumn
,将 host 主机上的某个系统目录挂载到了容器的某个目录上,那么容器就可以对 host 主机上的系统目录进行更改,从而发起攻击;
后续会介绍,Docker 是如何使用 User Namespace 来规避这种风险的,但在 Docker 1.10 版本之前,Docker 是不支持 user namespace,所以,旧的版本是非常容易被黑客所利用的..
下面,我们来重现这种风险的过程,注:我本地 Docker 版本是 1.12.3,没有开启 User Namespace 映射。
启动容器 web31,并将 host 的 /bin 挂载到了容器的 /host/bin 中1
mac@ubuntu:~$ docker run -d -v /bin:/host/bin --name web34 training/webapp python app.py
1 | mac@ubuntu:~$ docker inspect web34 |
我们来试图通过修改容器中的 /host/bin 中的文件已达到对 host /bin 目录中的文件修改,
1 | mac@ubuntu:~$ docker exec -it web34 bash |
返回 host,查看 /bin 目录下的情况,1
2mac@ubuntu:~$ ls /bin/hackfile
/bin/hackfile
可以看到,在容器中成功的在 host /bin 目录下生成了 hackfile 已达到了攻击的目的。所以,容器和主机共享一个 root 账户是非常非常危险的
,下面,我们来看看,Docker 是如何通过 User Namespace 来避免这种情况的。
启用 Host 用户与 Container 用户映射
启用 Host 用户与 Container 用户的映射,步骤如下,
- 修改 /etc/default/docker 文件,添加行 DOCKER_OPTS=”–userns-remap=default”
- 重启 Docker,
sudo service docker start
记录一个坑
,如果你的 Unbuntu 是 16+ 版本,Docker 1.12.5 版本的,上述的配置默认是不会生效的。我们需要继续做如下的修改,
- 修改 /lib/systemd/system/docker.service,做如下修改
- 添加一行 EnvironmentFile=-/etc/default/docker (- 代表ignore error)
- 将ExecStart=/usr/bin/docker daemon -H fd:// 改成 ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS
- 重启 Docker
systemctl daemon-reload
sudo service docker restart
查看 dockerd 进程,发现参数--userns-remap=default
成功添加在了启动 CMD 中1
2
3root@ubuntu:~# ps -ef | grep dockerd
root 14225 1 1 15:43 ? 00:00:00 /usr/bin/dockerd -H fd:// --userns-remap=default
root 14309 12300 0 15:43 pts/0 00:00:00 grep --color=auto dockerd
好了,User 映射启动了,再次使用上述的用例来进行测试,记得将之前的 web34 Container 停止后删除。1
mac@ubuntu:~$ docker run -d -v /bin:/host/bin --name web34 training/webapp
再次 hack,1
2
3
4root@ubuntu:~# docker exec -it web34 bash
root@4129f02954e7:/opt/webapp# cd /host/bin
root@4129f02954e7:/host/bin# touch new_hackfle
touch: cannot touch 'new_hackfle': Permission denied
是的,这次你得到了Permission denied
的错误提示,表示你当前没有权限更改 host /bin 目录上的文件了。
我们来看看到底发生了什么?1
2root@4129f02954e7:/host/bin# id
uid=0(root) gid=0(root) groups=0(root)
可见,容器中,依然使用的是 root 账户,uid=0;那么 host 是如何去映射 Container 中的 root 用户的呢?
1 | root@ubuntu:~# ps -ef | grep python |
host 主机上,对应容器的 init 进程的进程(14768
)用户,已经不再是 host 的 root 用户了,而是由 host 的另一个用户231072
所替代了。我们看看,host 是如何将用户 231072
与 Container 中的 root 用户进行映射的?1
2root@ubuntu:~# cat /proc/14768/uid_map
0 231072 65536
可见,host 将容器的 root 用户( UID = 0 )映射为了 host 的 231072
用户,所以,当容器试图再次进行 hack 的 host /bin 的时候,实际上是使用的用户231072
在执行,所以返回Permission Denied
。
其实归纳起来,就是这样的关系 host 231072
<--> 容器 root 用户;-->