1. 一个奇怪的需求
老板有个奇怪的需求,通过一个 kubeconfig 文件,获取主机的各种状态信息,比如进程列表、进程状态等。
第一反应就是,老板是不是不懂容器,容器怎么能这样玩,这样玩还要什么容器,内心万马奔腾。
直到最近遇到了一个命令行工具,才发现原来小丑是我自己。下面一起来看看,我发现了什么吧。
2. 容器的原理
沙箱是一个虚拟环境,在沙箱内部进行的操作对外部没有影响。沙箱与沙箱之间是隔离的,也是不可见的,看不到彼此的存在。
我们常说的容器就是基于 Linux 的 Cgroups 和 Namespace 技术构建的一个沙箱环境。
从图中,可以看到,容器与容器的边界就是通过 Cgroups 和 Namespace 这两种技术控制的。下面简单描述一下这两种技术:
- Namespace
不同 Namespace 下的资源相互独立、不可见。Linux 从 2.4.19 完成了支持 Mount Namespace,2.6.19 完成了支持 UTS、IPS Namespace,2.6.24 完成了支持 PID Namespace,2.6.29 完成了支持 Network Namespace,3.8 完成了支持 User Namespace 。其中,除了 User Namespace ,其他都需要以 root 权限创建。同时,在 4.6 中已经新增了 Cgroup namespace,目前 RunC(Docker 提供的运行时) ,已经合并了相关的 PR: https://github.com/opencontainers/runc/pull/1916 。下面是其中的 7 种 Namespace。
Mount namespace,隔离文件系统挂载点。一个 Namespace 中,程序对文件的修改,只影响自身的文件系统,而对其他 Namespace 没有影响。
UTS namespace,隔离主机名和域名信息。每个 Namespace 中,主机和域名信息相互独立。
IPC namespace,隔离进程通信的行为。只有一个 Namespace 中的进程可以互相通信。
PID namespace,隔离进程的 PID 空间。不同 Namespace 中的进程 PID 可以重复,互不影响。PID 为 1 的进程是其他所有进程的父进程,因此这个 Namespace 非常有意义。
Network namespace,隔离网络资源。每个 Namespace 都具有独立的网络栈信息,容器运行时仿佛在一个独立的网络中。
User namespace,隔离用户和用户组。同一个用户在不同的 Namespce,可以具有不同的角色,用来保障安全性。
Cgroup namespace,隔离 Cgroup 的可见性。每个 Namespace 中,都具有独立的 cgroupns root 和 cgroup filesystem 视图。
- Cgroups
上面将一组进程放置到一个 Namespace,对外隔离,对内共享资源,接着使用 Cgroups 对其进行资源的控制。Cgroups 提供了四个功能:
- 资源限制。针对一个进程或进程组,设置资源消耗限制。比如内存超出限制,会导致申请内存失败。
- 资源统计。统计 CPU 使用时长、内存用量等。
- 任务控制。控制进程的状态,可以挂起、恢复进程。
- 优先级分配。设置进程的优先级。
利用 Namespcae 和 Cgroups 提供的沙箱环境,再加上文件系统技术,就支持起了容器技术。
3. 一个调试工具: nsenter
nsenter 是一个用来进入指定程序,所在 Namespace,并执行命令的工具。在容器场景下,很多容器为了轻量化,而裁剪了很多基础的命令,比如 ip
、 tcpdump
等。这样给调试容器带来了一定的困难,通过 nsenter 共享 Namespace 进行调试,可以很好地解决这个问题。
实际上,RunC 在创建容器时,也是调用的 nsenter ,在 libcontainer 的代码中可以看到。
- 安装 nsenter
大部分的 Linux 操作系统,已经内置了 nsenter 命令。如果没有,以 CentOS 为例,执行如下命令,安装 util-linux
包即可:
1
| yum install -y util-linux
|
- nsenter 的版本和参数
由于不同的 Linux Kernel 对 Namespace 支持的程度不一样,nsenter 的版本会有所差异。这里以 CentOS 7 为例:
1
2
3
| uname -a
Linux i-x29a8rdc 3.10.0-1127.10.1.el7.x86_64 #1 SMP Wed Jun 3 14:28:03 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
|
1
2
3
| nsenter -V
nsenter from util-linux 2.23.2
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| nsenter -h
Usage:
nsenter [options] <program> [<argument>...]
Run a program with namespaces of other processes.
Options:
-t, --target <pid> target process to get namespaces from
-m, --mount[=<file>] enter mount namespace
-u, --uts[=<file>] enter UTS namespace (hostname etc)
-i, --ipc[=<file>] enter System V IPC namespace
-n, --net[=<file>] enter network namespace
-p, --pid[=<file>] enter pid namespace
-U, --user[=<file>] enter user namespace
-S, --setuid <uid> set uid in entered namespace
-G, --setgid <gid> set gid in entered namespace
--preserve-credentials do not touch uids or gids
-r, --root[=<dir>] set the root directory
-w, --wd[=<dir>] set the working directory
-F, --no-fork do not fork before exec'ing <program>
-Z, --follow-context set SELinux context according to --target PID
-h, --help display this help and exit
-V, --version output version information and exit
|
这里需要注意的是 -t
参数,指定一个进程,用于获取 Namepace 参数。其他参数主要是使能、设置参数。
由于非沙箱环境下,并不容易体现 nsenter 的功能,我们在容器环境下进一步实验。
4. nsenter 在 Docker 容器环境下的应用
4.1 主机下,进入容器的 Namespace 环境
1
2
3
4
| docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9addecf82c5e sonarqube:7.9.4-community "./bin/run.sh" 3 weeks ago Up 3 weeks 0.0.0.0:9000->9000/tcp sonarqube_sonarqube_1
|
每个容器内都有一个 PID=1 的进程,如同宿主机上的 init 进程一样,是其他进程的父进程。但是在主机上,容器进程具有另外一个 PID ,可以用于管理容器。
1
2
3
| docker inspect --format "{{ .State.Pid }}" 9addecf82c5e
3969
|
这里以进入网络空间为例:
1
| nsenter -t 3969 -n /bin/bash
|
如果宿主机上的默认 shell,在容器中存在,可以省略 /bin/bash
,否则需要显式指定一个容器中的 shell。
1
2
3
4
5
6
7
8
9
10
| ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
|
从执行结果可以看到,显示的是容器上的网卡地址信息,但 ip
命令却来自宿主机。
4.2 容器下,进入主机的 Namespace 环境
- 以特权模式,使用主机的 Namespace 创建容器
1
| docker run --privileged --net=host --ipc=host --pid=host -it --rm docker.io/alpine:3.12 /bin/sh
|
- 进入 PID=1 进程的 Namespace 环境
1
| nsenter -t 1 -m -u -i -n
|
1
2
3
4
5
| docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2cd99b9d7b5a alpine:3.12 "/bin/sh" About a minute ago Up About a minute trusting_khorana
9addecf82c5e sonarqube:7.9.4-community "./bin/run.sh" 3 weeks ago Up 3 weeks 0.0.0.0:9000->9000/tcp sonarqube_sonarqube_1
|
从执行结果可以看到,命令显示的是主机上的容器信息,但却是在容器下执行的命令。
5. nsenter 在 Kubernetes 容器环境下的应用
这部分的内容和上一个章节类似,只不过在进入容器时,需要借道 Pod 获取 PID;在主机上执行命令时,需要借道 Pod 创建容器。
5.1 从主机进入 Kubernetes Pod 中,调试容器环境
1
2
3
4
| kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-6db489d4b7-589bd 1/1 Running 0 11s 10.233.76.91 tf-cd-allinone-0 <none> <none>
|
1
2
3
| kubectl describe pod nginx-6db489d4b7-589bd | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'
981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1
|
1
| ssh root@tf-cd-allinone-0
|
1
2
3
| docker inspect --format "{{.State.Pid}}" 981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1
6954
|
1
2
3
4
5
6
7
8
9
10
11
12
| ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
link/ether 16:a3:44:dc:58:ce brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.233.76.91/32 scope global eth0
valid_lft forever preferred_lft forever
|
这里需要注意的是,容器和节点是绑定在一起的,对于多节点环境,获取容器 ID 之后,需要切换到所在主机进行操作。
5.2 在 Kubernetes Pod 中,直接操作主机
- 新建一个 pod-test.yaml 文件,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| apiVersion: v1
kind: Pod
metadata:
name: pod-test
namespace: default
spec:
containers:
- command: ['sh', '-c', 'echo "Hello, wwww.chenshaowen.com !" && sleep 3600']
image: docker.io/alpine:3.12
name: pod-test
securityContext:
privileged: true
hostIPC: true
hostNetwork: true
hostPID: true
|
1
| kubectl apply -f pod-test.yaml
|
1
| kubectl exec -it pod-test /bin/sh
|
1
| nsenter -t 1 -m -u -i -n -p
|
1
2
3
4
5
6
| docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6bd778c3172 389fef711851 "sh -c 'echo \"Hello,…" 4 minutes ago Up 4 minutes k8s_pod-test_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
4e197fd98294 kubesphere/pause:3.1 "/pause" 4 minutes ago Up 4 minutes k8s_POD_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
...
|
以特权模式启动容器,通过 PID=1 的进程共享 Namespace,直接执行主机上的命令。
6. 总结
本篇主要介绍了在容器环境下,如何逃逸到主机执行命令;在主机下,如何进入容器调试环境。同时,还给出了在 Container 和 Kubernetes 两种场景下的实践示例。
其中有两点对我有所启发,一个是 nsenter 命令,加深了对容器技术的理解。另外一个是特权模式启动的容器,权限十分大,需要谨慎,业务应该尽量采用 rootless 的方式运行容器。
在以特权模式启动的 Docker Daemon 中,创建 Kuberntes 集群,通过 nsenter 命令,可以 nodeSelector 到任意节点,然后执行 kubectl/docker/systemctl 等命令进行破坏活动。
7. 参考