本記事について
この記事で書かれている内容はRestrict a Container's Syscalls with seccompをベースにしていました。このドキュメントのうち前半の指定した任意のSeccompプロファイルを使ってPodを作成する機能は1.25でデフォルトで使えるようになった機能ではなく、以前から使えるものです。
タイトルが紛らわしいと思いますので、「旧タイトル: Kubernetes 1.25でデフォルト有効化された「SeccompDefault」を試してみる」から「Kubernetes 1.25でデフォルト有効化された「SeccompDefault」機能で「RuntimeDefault」を試してみる」に変更しています。
初稿で不足している部分を「8/25追記 もう少し深堀り」という後で追記しています。
Kubernetes 1.25がリリースされましたね!
Kubernetes 1.25がリリースされたので、kubeadmとCRI-Oの組み合わせで評価環境を作ってみることにしました。 CRI-OはKubernetesのリリースに合わせて同じバージョンのCRI-Oをリリースしていますが、現時点ではCRI-O 1.25がまだ利用可能ではなかったので、CRI-O 1.24を利用しています。
kubeadmなどのインストールについてはいつものようにInstalling kubeadm、ランタイムのインストールについてはContainer Runtimesに従ってセットアップしています。
早速kubeadmでクラスタ作成!
早速クラスターを作成してみます。
$ sudo kubeadm init --kubernetes-version 1.25.0 --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint=172.16.214.190 [init] Using Kubernetes version: v1.25.0 [preflight] Running pre-flight checks [WARNING Swap]: swap is enabled; production deployments should disable swap unless testing the NodeSwap feature gate of the kubelet [WARNING FileExisting-tc]: tc not found in system path [WARNING SystemVerification]: missing optional cgroups: blkio error execution phase preflight: [preflight] Some fatal errors occurred: [ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables does not exist [ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1 [preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...` To see the stack trace of this error execute with --v=5 or higher
失敗です!
1つ目の問題「SWAP」
ワーニングなのでそのままでも動くっぽいですが、Fedora 33以降の場合はZRAMを使っているので、次のドキュメントにあるように場合によっては無効化したほうが良いかもしれません。
今回はちょっとメモリの割当が小さいVM(vCPU2、メモリー4GB)で動かしてしまったので、想定したように動かない限りはオンのままにしようと思いました。
2つ目の問題「tcコマンドがない」
入っていなければ入れれば良いです。以上。
$ sudo dnf provides tc Last metadata expiration check: 0:02:58 ago on Wed 24 Aug 2022 02:20:45 AM UTC. iproute-tc-5.15.0-2.fc36.x86_64 : Linux Traffic Control utility Repo : fedora Matched from: Filename : /usr/sbin/tc Provide : /sbin/tc $ sudo dnf install iproute-tc
3つ目の問題「ブリッジ周りの設定」
「Forwarding IPv4 and letting iptables see bridged traffic」のところの設定をするのを忘れていたので、実施しました。
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # sysctl params required by setup, params persist across reboots cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF # Apply sysctl params without reboot sudo sysctl --system
もう一度クラスタ作成をトライ
ここまで設定した上で、もう一度クラスター作成を試しました。
$ sudo kubeadm init --kubernetes-version 1.25.0 --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint=172.16.214.190 [init] Using Kubernetes version: v1.25.0 [preflight] Running pre-flight checks [WARNING Swap]: swap is enabled; production deployments should disable swap unless testing the NodeSwap feature gate of the kubelet [WARNING SystemVerification]: missing optional cgroups: blkio ... [kubelet-check] It seems like the kubelet isn't running or healthy. [kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get "http://localhost:10248/healthz": dial tcp [::1]:10248: connect: connection refused. [kubelet-check] It seems like the kubelet isn't running or healthy. [kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get "http://localhost:10248/healthz": dial tcp [::1]:10248: connect: connection refused. [kubelet-check] It seems like the kubelet isn't running or healthy. [kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get "http://localhost:10248/healthz": dial tcp [::1]:10248: connect: connection refused. [kubelet-check] It seems like the kubelet isn't running or healthy. [kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get "http://localhost:10248/healthz": dial tcp [::1]:10248: connect: connection refused.
healthzがどうのこうのというエラーが出る場合は、Kubeletが正常に動いていないようです。CRI-Oの設定を以下を確認して見直しました。
ランタイム側とKubeletのcgroup_managerの設定を合わせる必要があるのですが、上のドキュメントをみて何も考えずにcgroupfs
を設定して、数十分ウンウン唸る事になってしまいました。
最終的には次のような設定にしました。
$ sudo vi /etc/crio/crio.conf [crio.runtime] conmon_cgroup = "pod" cgroup_manager = "systemd" [crio.image] pause_image="registry.k8s.io/pause:3.6"
またこのあともう一度kubeadm init
を実行したのですがうまく行かず、journalctlでkubeletの状況を確認したら「SWAPをオフにしてくれ!」とkubeletが叫んでいました。kubeadm initのときにそう言っていただければ やっぱりオフにしたほうが良さそうです。
$ journalctl -xeu kubelet kubelet[5474]: E0824 03:26:42.012686 5474 run.go:74] "command failed" err="failed to run Kubelet: running with swap on is not supported, please disable swap! or set --fail-swap-on flag to false. /proc/swaps contained
Fedoraの場合、スワップオフの方法は次に書かれているように、sudo dnf remove zram-generator-defaults
を実行すれば良いそうです。
クラスター作成3度目
設定変更により、ようやくクラスター作成に成功しました。
$ sudo kubeadm init --kubernetes-version 1.25.0 --pod-network-cidr=10.244.0.0/16 --control-plane-endpoint=172.16.214.190 ... Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Alternatively, if you are the root user, you can run: export KUBECONFIG=/etc/kubernetes/admin.conf You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ ...
ネットワークアドオンの適用
うまくクラスターを作れたので、あとは画面出力にあるように設定を順にやっていきます。 ネットワークアドオンはFlannelにしました。
$ kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
コントロールプレーンノードの分離
今回はちょっと動かすだけなので、シングルノードで動かすためのいつもの設定を行います。以前のバージョンとちょっと指定するオプションが変わっているので注意です。
実行するコマンドは次の通りです。例のように「node/** untainted」のように出力があれば成功です。
$ kubectl taint nodes --all node-role.kubernetes.io/control-plane- node/k8s-cplane.novalocal untainted $ kubectl get node NAME STATUS ROLES AGE VERSION k8s-cplane.novalocal Ready control-plane 9m59s v1.25.0
基本動作の確認
適当にアプリを動かしてみます。問題なければアプリは実行できるはずです。
$ kubectl create deployment hello-node --image=k8s.gcr.io/echoserver:1.4 deployment.apps/hello-node created $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE hello-node 1/1 1 1 102s
「seccompを使用したコンテナのSyscall制限」を試す
こちらを参考に、コンテナランタイムのデフォルトのseccompプロファイルを使用してPodを作成してみましょう。
このマニフェストを使ってPodを作成してみます。
apiVersion: v1 kind: Pod metadata: name: default-pod labels: app: default-pod spec: securityContext: seccompProfile: type: RuntimeDefault containers: - name: test-container image: hashicorp/http-echo:0.2.3 args: - "-text=just made some more syscalls!" securityContext: allowPrivilegeEscalation: false
ちなみにCRI-Oの場合は
ちなみにCRI-Oの場合はデフォルトはCRI-O内蔵のseccompプロファイルが使われるようです。内容的には何もいじっていないPodmanをインストールしたときに/usr/share/containers/seccomp.json
がランタイムのデフォルトプロファイルです。/usr/share/containers/seccomp.json
にコピーされるものと一緒の内容になると思います。
Podを作成してみます。
$ kubectl create -f default-pod.yaml pod/default-pod created $ kubectl get -f default-pod.yaml NAME READY STATUS RESTARTS AGE default-pod 1/1 Running 0 6s
問題なくPodを作成できました。securityContextでseccompProfileのタイプをRuntimeDefaultに設定しています。 アプリケーションは、ランタイムのSeccomp設定で定義したシステムコールのルールに従って実行されています。
8/25追記 もう少し深堀り
CRI-Oの設定は/etc/crio/crio.conf
です。/etc/containers
や/user/share/containers
はPodmanやBuildah、Skopeoなどが主に使う設定ファイルです。CRI-Oの設定を開くとわかりますが、ストレージの設定は/etc/containers/storage.conf
を参照するようです。
というわけで、/etc/crio/crio.conf
の設定を今一度確認してみようと思います。
設定の「seccomp_profile」は、seccompファイルを指定する設定です。 指定していない場合は、内蔵のデフォルトseccompファイルが使われるようです。次がデフォルトの設定でした。
# Path to the seccomp.json profile which is used as the default seccomp profile # for the runtime. If not specified, then the internal default seccomp profile # will be used. This option supports live configuration reload. # seccomp_profile = ""
ここは空のseccompファイルを指定した場合、無制限になるのを デフォルトプロファイルに置き換えるという設定です。それにより、セキュリティが向上する可能性があるそうです。次がデフォルトの設定でした。
# Changes the meaning of an empty seccomp profile. By default # (and according to CRI spec), an empty profile means unconfined. # This option tells CRI-O to treat an empty profile as the default profile, # which might increase security. # seccomp_use_default_when_empty = true
さらに下が許可するケーパビリティとシステムコールの設定です。 あえてここに"SYS_CHROOT"を追加しない限り、デフォルト設定のままだとchrootはこちらで阻止されます。
# List of default capabilities for containers. If it is empty or commented out, # only the capabilities defined in the containers json file by the user/kube # will be added. # default_capabilities = [ # "CHOWN", # "DAC_OVERRIDE", # "FSETID", # "FOWNER", # "SETGID", # "SETUID", # "SETPCAP", # "NET_BIND_SERVICE", # "KILL", # ] # List of default sysctls. If it is empty or commented out, only the sysctls # defined in the container json file by the user/kube will be added. # default_sysctls = [ # ]
今回検証のために、次のような設定を仕込みました。設定の優先順(?)の都合で、ホストのケーパビルティでは許可して、seccompファイルでchrootの実行を阻止するような設定にしています。
seccomp_profile = "/etc/crio/dropchroot.json" seccomp_use_default_when_empty = false default_capabilities = [ "CHOWN", "SYS_CHROOT", #追加したケーパビリティ "DAC_OVERRIDE", "FSETID", "FOWNER", "SETGID", "SETUID", "SETPCAP", "NET_BIND_SERVICE", "KILL", ]
上で指定したdropchroot.json
は、デフォルトのSeccompと思われるファイル(/usr/share/containers/seccomp.json)に次を追記しただけのファイルです。
"syscalls": [ { "names": [ "bdflush", "chroot", #追記した行 ... ], "action": "SCMP_ACT_ERRNO", #これらのsyscall要求は阻止 "args": [], "comment": "", "includes": {}, "excludes": {}, "errnoRet": 1, "errno": "EPERM" },
次のようなマニフェストでPodを作成してみます。
profiles/
はこの環境ではフルパスとして/var/lib/kubelet/seccomp/profiles/
が参照されるようです。
$ cat vol-waru5.yaml apiVersion: v1 kind: Pod metadata: name: pv-waru5 namespace: default spec: restartPolicy: Never securityContext: seccompProfile: type: Localhost localhostProfile: profiles/dropchroot.json volumes: - name: vol5 hostPath: path: / containers: - name: pv-waru5 image: "k8s.gcr.io/busybox" tty: true volumeMounts: - name: vol5 mountPath: /rootfs
想定通り、chrootの実行は阻止してくれます。
$ kubectl create -f vol-waru5.yaml $ kubectl exec -it pv-waru5 -- chroot /rootfs chroot: can't change root directory to '/rootfs': Operation not permitted command terminated with exit code 1
次にこのようなマニフェストファイルを作成します。
$ cat vol-waru6.yaml apiVersion: v1 kind: Pod metadata: name: pv-waru6 namespace: default spec: restartPolicy: Never securityContext: seccompProfile: type: RuntimeDefault volumes: - name: vol6 hostPath: path: / containers: - name: pv-waru6 image: "k8s.gcr.io/busybox" tty: true volumeMounts: - name: vol6 mountPath: /rootfs
これも同様に阻止してくれます。
$ kubectl create -f vol-waru6.yaml $ kubectl exec -it pv-waru6 -- chroot /rootfs chroot: can't change root directory to '/rootfs': Operation not permitted command terminated with exit code 1
次にこのようなマニフェストファイルを作成します(securityContextを記述していないマニフェスト)。
$ cat vol-waru7.yaml apiVersion: v1 kind: Pod metadata: name: pv-waru7 namespace: default spec: restartPolicy: Never volumes: - name: vol7 hostPath: path: / containers: - name: pv-waru7 image: "k8s.gcr.io/busybox" tty: true volumeMounts: - name: vol7 mountPath: /rootfs
pv-waru6と同じ結果を期待していましたが、この場合は阻止してくれませんでした。
$ kubectl create -f vol-waru7.yaml $ kubectl exec -it pv-waru7 -- chroot /rootfs Failed to connect to bus: No data available basename: missing operand Try 'basename --help' for more information. [root@pv-waru7 /]#
Podに対してどのようなケーパビリティが設定されているかはcrictl inspectp
で確認できます。
$ sudo crictl pods POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME 254ab4917621f 4 minutes ago Ready pv-waru7 default 0 (default) 2a3adfa877a49 38 minutes ago Ready pv-waru5 default 0 (default) a868e5b96d270 39 minutes ago Ready pv-waru6 default 0 (default) 76605e78bc572 3 hours ago Ready coredns-565d847f94-t9mnc kube-system 0 (default) 4671f6d19e376 3 hours ago Ready coredns-565d847f94-hgp8w kube-system 0 (default) 15dd2f5bb2afc 3 hours ago Ready kube-proxy-ptmb4 kube-system 0 (default) 055f55bbb0e7f 3 hours ago Ready kube-flannel-ds-ts4cn kube-flannel 0 (default) 617e6274eaa1c 3 hours ago Ready etcd-k8s-cplane.novalocal kube-system 0 (default) 31591004ece02 3 hours ago Ready kube-scheduler-k8s-cplane.novalocal kube-system 0 (default) 1cf4de5c81987 3 hours ago Ready kube-controller-manager-k8s-cplane.novalocal kube-system 0 (default) ba5c6071d80c2 3 hours ago Ready kube-apiserver-k8s-cplane.novalocal kube-system 0 (default)
例えばつぎのように...
$ sudo crictl inspectp 2a3adfa877a49 #pv-waru5 $ sudo crictl inspectp a868e5b96d270 #pv-waru6 $ sudo crictl inspectp 254ab4917621f #pv-waru7
各Podのコンフィグについておさらいすると...
- pv-waru5は任意のseccompファイルを指定してPodを作成
- pv-waru6 はRuntimeDefaultを設定
- pv-waru7 はseccompProfileを指定していない
crictl inspectp
を実行すると、Podのいろいろな情報の中にcapabilities
、effective
、permitted
、syscalls
などの情報が含まれています。
pv-waru7にもcapabilities、effective、permitted、syscallsなどは設定されていたものの、なぜかchroot /rootfs
は成功してしまいました。
しかしCRI-Oのデフォルト設定であればセキュアなので、今回テストのために設定変更した各項目をもとの設定に戻せば抜けてしまったパターンでも問題が再現することはないでしょう。
$ sudo vi /etc/crio/crio.conf ... seccomp_profile = "" ... seccomp_use_default_when_empty = true ... default_capabilities = [ "CHOWN", "DAC_OVERRIDE", "FSETID", "FOWNER", "SETGID", "SETUID", "SETPCAP", "NET_BIND_SERVICE", "KILL", ] ...
設定変更後、crioとkubeletサービスを再起動します。
$ sudo systemctl restart crio kubelet
もう一度pv-waru7 を実行してみましょう。今度は阻止してくれました。これはCRI-Oのケーパビリティのほうが効いている影響だと思います。
$ kubectl create -f vol-waru7.yaml $ kubectl exec -it pv-waru7 -- chroot /rootfs chroot: can't change root directory to '/rootfs': Operation not permitted command terminated with exit code 1
同様に、crictl inspectp
でPodの状況を確認してみましょう。
$ sudo crictl pods POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME 7ed614b8103c9 About a minute ago Ready pv-waru7 default 0 (default) ... $ sudo crictl inspectp 7ed614b8103c9 ... { "names": [ "chroot" ], "action": "SCMP_ACT_ERRNO", "errnoRet": 1 },
chrootのケーパビリティはPodに対してきちんと設定されており、想定通り動作するのを確認しました。 ただ、正直言ってseccomp_profileをカスタマイズしてセキュリティを担保するのは一般のユーザーには難しいのではないかと思います(もちろん私も)。CRI-Oのようにケーパビリティで設定できたほうが良いと感じます。ここは他のランタイムにはあんまりないんですよね。そういう意味では基本的な設計が同一のPodmanは安心して使えそうです。
(以上加筆終わり)
seccomp を使用してコンテナの Syscall を制限する利点
「コンテナはLinux kernelをホストと共有するので、仮想マシンのようなオーバーヘッドが少ないというメリットがある」を裏返すと、「仮想マシンのようにホストと完全に隔離していないので、コンテナアプリケーションだったりLinux kernel、コンテナなどの脆弱性をついて情報の漏洩、悪用ができてしまう可能性がある」といえます。
この対策としてLinuxのセキュリティ機能(AppArmorやSELinux)や seccomp(セキュアコンピューティングモード)などを使うという方法があります。あとはrootユーザーでコンテナアプリケーションを実行しないというのは前々からTipsとしてありましたね。
これらの機能はランタイム側はすでに実装されている(注1)ものの、Kubernetesではあくまでオプション機能という位置づけであり、デフォルトでは利用されませんでした。Kubernetesのセキュリティ周りは管理者のポリシーに委ねられていました。
Kubernetes 1.22で初期実装され、今回1.25のリリースでデフォルトで有効化されたわけです。
ランタイム開発者が安全にコンテナーを実行するための設定をランタイム側で実装しているため、今回Kubernetes 1.25でこの機能が有効化されたことで、Kubernetesでしばしば報告される潜在的なエクスプロイトを無意味にできます。
注1: CRI-Oとコードの多くを共有しているPodmanでは、SELinuxが有効のマシン上でSELinuxを有効にした状態でコンテナーを動かすことができ、SELinuxが対応できるエクスプロイトはブロックできます。Seccompもデフォルトで有効化されていますし、もう少し簡単な方法でシステムコールの許可/不可を設定できます。DockerやcontainerdはSeccompについてはデフォルトが定義されていたはずですが、設定変更の方法やSELinux関連については情報を見つけられていません。