コンテナ専用OS Talos Linuxを用いたKubernetesクラスタ(検証編)
この記事の概要
本記事は以下記事の続きです。
Talos Linuxを用いて構築したKubernetesクラスタに対して、いくつか検証を行います。
APIによる操作
Talos LinuxにはSSHやBashが含まれていないため、一般的によく行われるSSHを経由したサーバーの操作を行うことができません。
代わりにTalos LinuxではAPIが公開されているため、サーバーの操作はtalosctlを用いて、API経由(gRPC)で行うことになります。
実行可能な操作は、talosctl -h
コマンドを用いて確認できます。
$ talosctl -h
A CLI for out-of-band management of Kubernetes nodes created by Talos
Usage:
talosctl [command]
Available Commands:
apply-config Apply a new configuration to a node
bootstrap Bootstrap the etcd cluster on the specified node.
cluster A collection of commands for managing local docker-based or QEMU-based clusters
completion Output shell completion code for the specified shell (bash, fish or zsh)
config Manage the client configuration file (talosconfig)
conformance Run conformance tests
containers List containers
copy Copy data out from the node
dashboard Cluster dashboard with node overview, logs and real-time metrics
disks Get the list of disks from /sys/block on the machine
dmesg Retrieve kernel logs
...
EndpointとNode
talosctl
を用いる際には--nodes
(-n
)と--endpoints
(-e
)というフラグを指定する必要があります。
それぞれの意味は以下の通りです。
-
--endpoints
:talosctl
でリクエストを送信するエンドポイントを指定する(あらかじめtalosconfig
の中で定義しておくことも可能) -
--nodes
: 操作の対象とするサーバーを指定する
エンドポイント(--endpoints
)で受け付けたリクエストは、実際の操作対象であるサーバー(--nodes
)に転送されます。
Kubernetesクラスタを構築する場合、kubectl
からリクエストを受け付けるエンドポイントはLoadBalancerを介して公開されることが多く、Control Plane
やNode
についてはユーザーからリクエストを受け付ける必要はないため、ユーザーから直接アクセスできないネットワークに配置されるのが一般的です。
このようなケースでも、--endpoint
にユーザーからアクセス可能なLoadBalancerを指定し、--nodes
に操作対象のサーバーを指定することで、talosctl
を使用してユーザーから直接アクセスできないControl Plane
やNodeの操作をAPI経由で行うことができるようになっています。
※Endpoints and Nodesより引用
API経由操作を行う例
例えばサーバー上で実行されているServiceのログを確認したい場合は、talosctl log
コマンドを使用することで確認することができます。
以下はkubeletのログを確認する例です。
$ talosctl --talosconfig=./talosconfig -e k8s-talos-cp01 -n k8s-talos-cp01 logs kubelet
k8s-talos-cp01: {"ts":1719422847963.3943,"caller":"app/server.go:484","msg":"Kubelet version","v":0,"kubeletVersion":"v1.30.1"}
k8s-talos-cp01: {"ts":1719422847963.4224,"caller":"app/server.go:486","msg":"Golang settings","v":0,"GOGC":"","GOMAXPROCS":"","GOTRACEBACK":""}
k8s-talos-cp01: {"ts":1719422847963.6057,"caller":"app/server.go:927","msg":"Client rotation is on, will bootstrap in background","v":0}
k8s-talos-cp01: {"ts":1719422847965.8086,"caller":"dynamiccertificates/dynamic_cafile_content.go:157","msg":"Starting controller","v":0,"name":"client-ca-bundle::/etc/kubernetes/pki/ca.crt"}
...
また、サーバーの設定を変更したい場合は、machine config
に設定を追加し、talosctl apply-configを実行します。(この他にもtalosctl editやtalosctl patchといったコマンドを利用することもできます。)
例えばサーバーの/etc/hosts
にレコードを追記したい場合は、次のように.network.extraHostEntries
にレコードを追加したmachine config
を作成します。(ここではcontrolplane.yaml
を例にしています。)
...
network:
...
# # Allows for extra entries to be added to the `/etc/hosts` file
extraHostEntries:
- ip: 192.168.2.140
aliases:
- k8s-talos-cp01
- ip: 192.168.2.141
aliases:
- k8s-talos-node01
- ip: 192.168.2.142
aliases:
- k8s-talos-node02
- ip: 192.168.2.143
aliases:
- k8s-talos-node03
...
用意したmachine config
を適用します。
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 apply-config -f controlplane.yaml
現時点では/etc/hosts
にレコードが反映されたかを確認することはできませんが、この後解説するデバッグPodを利用してサーバー上の/etc/hosts
を確認してみると、意図した通りレコードが追加されていることが確認できます。
root@k8s-talos-cp01:/# cat /host/etc/hosts
127.0.0.1 localhost
192.168.2.140 k8s-talos-cp01
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.2.140 k8s-talos-cp01
192.168.2.141 k8s-talos-node01
192.168.2.142 k8s-talos-node02
192.168.2.143 k8s-talos-node03
Talos Linuxのアーキテクチャ
Talos Linuxは次のようなコンポーネントで構成されています。
※Componentsより引用
各コンポーネントの状態は、talosctl service
コマンドで確認できます。
$ talosctl --talosconfig=./talosconfig -e k8s-talos-cp01 -n k8s-talos-cp01 service
NODE SERVICE STATE HEALTH LAST CHANGE LAST EVENT
k8s-talos-cp01 apid Running OK 68h7m55s ago Health check successful
k8s-talos-cp01 containerd Running OK 68h8m8s ago Health check successful
k8s-talos-cp01 cri Running OK 68h8m0s ago Health check successful
k8s-talos-cp01 dashboard Running ? 68h8m2s ago Process Process(["/sbin/dashboard"]) started with PID 1950
k8s-talos-cp01 etcd Running OK 68h3m19s ago Health check successful
k8s-talos-cp01 kubelet Running OK 68h7m47s ago Health check successful
k8s-talos-cp01 machined Running OK 68h8m13s ago Health check successful
k8s-talos-cp01 syslogd Running OK 68h8m12s ago Health check successful
k8s-talos-cp01 trustd Running OK 68h7m55s ago Health check successful
k8s-talos-cp01 udevd Running OK 68h8m3s ago Health check successful
$ talosctl --talosconfig=./talosconfig -e k8s-talos-cp01 -n k8s-talos-cp01 service kubelet
NODE k8s-talos-cp01
ID kubelet
STATE Running
HEALTH OK
EVENTS [Running]: Health check successful (68h8m4s ago)
[Running]: Started task kubelet (PID 2146) for container kubelet (68h8m6s ago)
[Preparing]: Creating service runner (68h8m6s ago)
[Preparing]: Running pre state (68h8m17s ago)
[Waiting]: Waiting for service "cri" to be "up" (68h8m17s ago)
[Waiting]: Waiting for service "cri" to be "up", time sync, network (68h8m18s ago)
[Starting]: Starting service (68h8m18s ago)
この中でも中心的な役割を担っているのがapid
、machined
という2つのコンポーネントです。
apid
はTalos Linuxを操作するためのAPIを公開しており、talosctl
コマンドを実行した際もここにリクエストが送信されます。
また、EndpointとNodeで解説したAPIリクエストを自分以外のサーバーに転送する役割も有しています。
因みにapid
はTalos Linux上でコンテナとして実行されています。(ただしKubernetesからはPodとして確認することができません。)
machined
はTalos Linuxのinitプロセスとして動作し、内部ではさまざまなControllerが実行されているようです。
先ほど解説した通りTalos Linuxに対する設定の反映は基本的にmachine config
を適用することにより行われますが、machine config
をTalos Linuxに適用すると、それに基づきmachineconfigs.config.talos.dev
(MachineConfig
)というリソースが作成されます。
※適用されたmachine config
はファイルとして/system/state/config.yaml
に保存されます。
※サーバーセットアップ時に設定したホスト名やIPアドレスなども/system/state/platform-network.yaml
にファイルとして保存されています。
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get machineconfig
NODE NAMESPACE TYPE ID VERSION
k8s-talos-cp01 config MachineConfig v1alpha1 1
これをControllerが読み取ってリソースを作成したり、リソースを読み取ったControllerがリソースで定義された状態とサーバーの状態を一致させるような処理(Reconcile)を行うといった具合で、サーバーの設定やファイルの作成、プロセスの起動等が行われます。
なお、作成されたリソースはMachineConfig
を除いて基本的にサーバーのメモリ上に保存され、サーバーが再起動された場合はMachineConfig
に基づき再作成されるようです。
Talos Linuxで定義されているリソースはtalosctl get resourcedefinitions
コマンドで確認できます。
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get resourcedefinitions
NODE NAMESPACE TYPE ID VERSION ALIASES
k8s-talos-cp01 meta ResourceDefinition acquireconfigspecs.v1alpha1.talos.dev 1 acquireconfigspec acs
k8s-talos-cp01 meta ResourceDefinition acquireconfigstatuses.v1alpha1.talos.dev 1 acquireconfigstatus acs
k8s-talos-cp01 meta ResourceDefinition addressspecs.net.talos.dev 1 addressspec as
k8s-talos-cp01 meta ResourceDefinition addressstatuses.net.talos.dev 1 address addresses addressstatus as
k8s-talos-cp01 meta ResourceDefinition adjtimestatuses.v1alpha1.talos.dev 1 adjtimestatus as
k8s-talos-cp01 meta ResourceDefinition admissioncontrolconfigs.kubernetes.talos.dev 1 admissioncontrolconfig acc accs
k8s-talos-cp01 meta ResourceDefinition affiliates.cluster.talos.dev 1 affiliate
...
以下の図は公式ドキュメントで示されているtalosctl inspect dependencies
コマンドを実行してControllerとリソースの関係を出力した結果です。
非常に多くのControllerが実行されていることが確認できます。
例えばここからkubeletに関するものを抜き出すと次のようになります。
machine config作成時に、machine configにkubelet
の設定が含まれていることを確認しました。
machine config
を適用するとMachineConfig.config.talos.dev
というリソースが作成されます。
その中からkubeletの設定に関連する箇所がk8s.KubeletConfigController
によりKubeletConfigs.kubernetes.talos.dev
というリソースに変換されます。
その後、k8s.KubeletSpecController
によりNodenames.kubernetes.talos.dev
(managed by k8s.nodeNameController
)やNodeIPs.kubernetes.talos.dev
(managed by k8s.NodeIPController
)とマージされ、KubeletSpecs.kubernetes.talos.dev
というリソースが生成されます。
このリソースをk8s.KubeletServiceController
が読み取ることで、kubeletが実行されるようです。
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get kubeletconfig
NODE NAMESPACE TYPE ID VERSION
k8s-talos-cp01 k8s KubeletConfig kubelet 1
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get kubeletconfig kubelet -oyaml
node: k8s-talos-cp01
metadata:
namespace: k8s
type: KubeletConfigs.kubernetes.talos.dev
id: kubelet
version: 1
owner: k8s.KubeletConfigController
phase: running
created: 2024-06-30T18:24:09Z
updated: 2024-06-30T18:24:09Z
spec:
image: ghcr.io/siderolabs/kubelet:v1.30.1
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local
cloudProviderExternal: false
defaultRuntimeSeccompEnabled: true
skipNodeRegistration: false
staticPodListURL: http://127.0.0.1:33389
disableManifestsDirectory: true
enableFSQuotaMonitoring: true
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get Nodenames
NODE NAMESPACE TYPE ID VERSION NODENAME
k8s-talos-cp01 k8s Nodename nodename 1 k8s-talos-cp01
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get NodeIPs
NODE NAMESPACE TYPE ID VERSION
k8s-talos-cp01 k8s NodeIP kubelet 1
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get KubeletSpecs
NODE NAMESPACE TYPE ID VERSION
k8s-talos-cp01 k8s KubeletSpec kubelet 1
その他にも、例えばstatic podとして実行されるkube-apiserver
やkube-controller-manager
、kube-scheduler
の情報についても、staticpods.kubernetes.talos.dev
というリソースとして管理されています。
$ talosctl --talosconfig=./talosconfig -n k8s-talos-cp01 -e k8s-talos-cp01 get staticpods.kubernetes.talos.dev
NODE NAMESPACE TYPE ID VERSION
k8s-talos-cp01 k8s StaticPod kube-apiserver 1
k8s-talos-cp01 k8s StaticPod kube-controller-manager 1
k8s-talos-cp01 k8s StaticPod kube-scheduler 1
このように、Talos LinuxではControllerとリソースを用いた所謂Operator Patternが用いられており、Kubernetesにとてもよく似た思想が反映されています。
デバッグPodを用いたNodeの調査
基本的にTalos Linuxに関する操作はAPIを経由して行う旨を記載しました。
しかし、Kubernetesではkubectl debug node
コマンドを用いることで、あたかもTalos Linuxとして実行されているNodeに接続したかのような状態を作り出すことができます。
詳細な仕組みは割愛しますが、NodeとNamespaceを共有したり、Nodeのrootfsを特定のディレクトリにhostPathでマウントした状態のデバッグPodをデプロイすることで、デバッグPodからNodeに対する操作を可能にしています。
ここではk8s-talos-cp01
に対してubuntu:22.04
のコンテナイメージを用いたデバッグPodをデプロイしています。
コマンドを実行すると、Nodeのホスト名やrootfs(デバッグPodの/host
にマウントされている)を参照できることが確認できます。
$ kubectl --kubeconfig ./alternative-kubeconfig debug node/k8s-talos-cp01 --profile=sysadmin -it --image=ubuntu:22.04 -- /bin/bash
Creating debugging pod node-debugger-k8s-talos-cp01-mtj6k with container debugger on node k8s-talos-cp01.
If you don't see a command prompt, try pressing enter.
root@k8s-talos-cp01:/#
root@k8s-talos-cp01:/# hostname
k8s-talos-cp01
root@k8s-talos-cp01:/# ls /host
bin boot dev etc lib mnt opt proc root run sbin sys system tmp usr var
例えばNodeの/bin
や/sbin
ディレクトリ配下を確認してみます。
一般的なLinuxディストリビューションであれば、/bin
のは以下にBashをはじめとした様々なコマンドが含まれていますが、Talos Linuxではコンテナランタイムのバイナリしか含まれていないことが確認できます。/sbin
の配下についてもコマンドがだいぶ絞られているようです。
root@k8s-talos-cp01:/# ls /host/bin/
containerd containerd-shim containerd-shim-runc-v2 runc
root@k8s-talos-cp01:/# ls /host/sbin/
blkdeactivate fsck.xfs ip6tables-legacy-save iptables-legacy-restore lvcreate lvmdevices lvremove modprobe pvmove udevadm vgconvert vgimportclone vgrename xtables-legacy-multi
dashboard init ip6tables-restore iptables-legacy-save lvdisplay lvmdiskscan lvrename poweroff pvremove udevd vgcreate vgimportdevices vgs
dmsetup ip6tables ip6tables-save iptables-restore lvextend lvmdump lvresize pvchange pvresize vgcfgbackup vgdisplay vgmerge vgscan
dmstats ip6tables-apply iptables iptables-save lvm lvmsadc lvs pvck pvs vgcfgrestore vgexport vgmknodes vgsplit
dmstats.static ip6tables-legacy iptables-apply lvchange lvm_import_vdo lvmsar lvscan pvcreate pvscan vgchange vgextend vgreduce wrapperd
fsadm ip6tables-legacy-restore iptables-legacy lvconvert lvmconfig lvreduce mkfs.xfs pvdisplay shutdown vgck vgimport vgremove xfs_repair
Nodeで実行されているプロセスも確認できます。
Talos Linuxのコンポーネントであるapid
やtrustd
がコンテナとして実行されているのに加え、Kubernetesクラスタのコンポーネントであるkubelet
についてもコンテナとして実行されているのが印象的でした。(kubelet
はNodeのdaemonとして実行されるのが一般的)
なお、これらのコンテナはcontainerdのsystem
というNamespaceで実行されているため、kubectl get
コマンドを使用しても参照することはできません。
※Kubernetesで管理されるコンテナはcontainerdのk8s.io
というNamespaceで実行されます。
※etcd
についてもcontainerdのsystem
というNamespaceでコンテナとして実行されているため、Kubernetesからは参照できません。(etcd
をコンテナとして実行する場合は、Kubernetesのkube-system
というNamespace配下でPod
として実行するのが一般的)
root@k8s-talos-cp01:/# ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 1 0.3 1.3 1374344 110980 ? Sl Jun26 14:49 /sbin/init
root 1098 0.0 0.5 1284232 47608 ? Sl Jun26 2:10 /bin/containerd --address /system/run/containerd/containerd.sock --state /system/run/containerd --root /system/var/lib/containerd
root 1101 0.0 0.0 1464 1328 ? S Jun26 0:00 /sbin/udevd --resolve-names=never
50 1950 0.1 0.8 1308420 67876 tty2 Ssl+ Jun26 4:20 /sbin/dashboard
root 1996 0.2 0.9 1288876 73720 ? Sl Jun26 11:12 /bin/containerd --address /run/containerd/containerd.sock --config /etc/cri/containerd.toml
root 2030 0.0 0.1 1238144 14448 ? Sl Jun26 0:08 /bin/containerd-shim-runc-v2 -namespace system -id trustd -address /system/run/containerd/containerd.sock
51 2070 0.0 0.6 1307460 56220 ? Ssl Jun26 0:12 \_ /trustd
root 2034 0.0 0.1 1238144 14248 ? Sl Jun26 0:08 /bin/containerd-shim-runc-v2 -namespace system -id apid -address /system/run/containerd/containerd.sock
50 2071 0.0 0.7 1307972 57828 ? Ssl Jun26 0:12 \_ /apid --enable-rbac --enable-ext-key-usage-check
root 2126 0.0 0.1 1238144 14168 ? Sl Jun26 0:09 /bin/containerd-shim-runc-v2 -namespace system -id kubelet -address /run/containerd/containerd.sock
root 2146 0.7 1.2 2215772 104192 ? Ssl Jun26 32:44 \_ /usr/local/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubeconfig --cert-dir=/var/lib/kubelet/pki --config=/etc/kubernetes/kubelet.yaml --hostname-override=k8s-talos-cp01 --kubeconfig=/etc/kubernetes/kubeconfig-kubelet --node-ip=192.168.2.140
root 2227 0.0 0.1 1237888 14164 ? Sl Jun26 0:10 /bin/containerd-shim-runc-v2 -namespace system -id etcd -address /run/containerd/containerd.sock
60 2247 1.0 1.7 11873988 142340 ? Ssl Jun26 42:39 \_ /usr/local/bin/etcd --advertise-client-urls=https://192.168.2.140:2379 --auto-tls=false --cert-file=/system/secrets/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/etcd --experimental-compact-hash-check-enabled=true --experimental-initial-corrupt-check=true --experimental-watch-progress-notify-interval=5s --initial-advertise-peer-urls=https://192.168.2.140:2380 --initial-cluster=k8s-talos-cp01=https://192.168.2.140:2380 --initial-cluster-state=new --key-file=/system/secrets/etcd/server.key --listen-client-urls=https://[::]:2379 --listen-peer-urls=https://[::]:2380 --name=k8s-talos-cp01 --peer-auto-tls=false --peer-cert-file=/system/secrets/etcd/peer.crt --peer-client-cert-auth=true --peer-key-file=/system/secrets/etcd/peer.key --peer-trusted-ca-file=/system/secrets/etcd/ca.crt --trusted-ca-file=/system/secrets/etcd/ca.crt
root 2283 0.0 0.1 1238144 14764 ? Sl Jun26 0:22 /bin/containerd-shim-runc-v2 -namespace k8s.io -id ab42eef4bae9da4ea934e2729920413d6e795ae43c9c21a0495aadba9ce350ce -address /run/containerd/containerd.sock
nobody 2350 0.0 0.0 996 640 ? Ss Jun26 0:00 \_ /pause
nobody 2485 2.7 4.5 1617196 370296 ? Ssl Jun26 117:24 \_ /usr/local/bin/kube-apiserver --admission-control-config-file=/system/config/kubernetes/kube-apiserver/admission-control-config.yaml --advertise-address=192.168.2.140 --allow-privileged=true --anonymous-auth=false --api-audiences=https://k8s-talos-cp01:6443 --audit-log-maxage=30 --audit-log-maxbackup=10 --audit-log-maxsize=100 --audit-log-path=/var/log/audit/kube/kube-apiserver.log --audit-policy-file=/system/config/kubernetes/kube-apiserver/auditpolicy.yaml --authorization-mode=Node,RBAC --bind-address=0.0.0.0 --client-ca-file=/system/secrets/kubernetes/kube-apiserver/ca.crt --enable-admission-plugins=NodeRestriction --enable-bootstrap-token-auth=true --encryption-provider-config=/system/secrets/kubernetes/kube-apiserver/encryptionconfig.yaml --etcd-cafile=/system/secrets/kubernetes/kube-apiserver/etcd-client-ca.crt --etcd-certfile=/system/secrets/kubernetes/kube-apiserver/etcd-client.crt --etcd-keyfile=/system/secrets/kubernetes/kube-apiserver/etcd-client.key --etcd-servers=https://localhost:2379 --kubelet-client-certificate=/system/secrets/kubernetes/kube-apiserver/apiserver-kubelet-client.crt --kubelet-client-key=/system/secrets/kubernetes/kube-apiserver/apiserver-kubelet-client.key --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --profiling=false --proxy-client-cert-file=/system/secrets/kubernetes/kube-apiserver/front-proxy-client.crt --proxy-client-key-file=/system/secrets/kubernetes/kube-apiserver/front-proxy-client.key --requestheader-allowed-names=front-proxy-client --requestheader-client-ca-file=/system/secrets/kubernetes/kube-apiserver/aggregator-ca.crt --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-group-headers=X-Remote-Group --requestheader-username-headers=X-Remote-User --secure-port=6443 --service-account-issuer=https://k8s-talos-cp01:6443 --service-account-key-file=/system/secrets/kubernetes/kube-apiserver/service-account.pub --service-account-signing-key-file=/system/secrets/kubernetes/kube-apiserver/service-account.key --service-cluster-ip-range=10.96.0.0/12 --tls-cert-file=/system/secrets/kubernetes/kube-apiserver/apiserver.crt --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256 --tls-min-version=VersionTLS12 --tls-private-key-file=/system/secrets/kubernetes/kube-apiserver/apiserver.key
root 2302 0.0 0.1 1238144 14916 ? Sl Jun26 0:22 /bin/containerd-shim-runc-v2 -namespace k8s.io -id 8bc14173a7321b034d6707c8e0e486e8d83de41346283169b06f539c08a6d95d -address /run/containerd/containerd.sock
65535 2353 0.0 0.0 996 640 ? Ss Jun26 0:00 \_ /pause
65535 2868 0.7 1.4 1335408 117360 ? Ssl Jun26 32:15 \_ /usr/local/bin/kube-controller-manager --use-service-account-credentials --allocate-node-cidrs=true --authentication-kubeconfig=/system/secrets/kubernetes/kube-controller-manager/kubeconfig --authorization-kubeconfig=/system/secrets/kubernetes/kube-controller-manager/kubeconfig --bind-address=127.0.0.1 --cluster-cidr=10.244.0.0/16 --cluster-signing-cert-file=/system/secrets/kubernetes/kube-controller-manager/ca.crt --cluster-signing-key-file=/system/secrets/kubernetes/kube-controller-manager/ca.key --configure-cloud-routes=false --controllers=*,tokencleaner --kubeconfig=/system/secrets/kubernetes/kube-controller-manager/kubeconfig --leader-elect=true --profiling=false --root-ca-file=/system/secrets/kubernetes/kube-controller-manager/ca.crt --service-account-private-key-file=/system/secrets/kubernetes/kube-controller-manager/service-account.key --service-cluster-ip-range=10.96.0.0/12 --tls-min-version=VersionTLS13
root 2309 0.0 0.1 1238400 14824 ? Sl Jun26 0:22 /bin/containerd-shim-runc-v2 -namespace k8s.io -id 958832fa98b9e53cdbeb67bca0db7811aca1d1b80b9120e30a8c5e9efb9a0e15 -address /run/containerd/containerd.sock
65536 2354 0.0 0.0 996 640 ? Ss Jun26 0:00 \_ /pause
65536 2831 0.1 0.7 1285716 62292 ? Ssl Jun26 6:12 \_ /usr/local/bin/kube-scheduler --authentication-kubeconfig=/system/secrets/kubernetes/kube-scheduler/kubeconfig --authentication-tolerate-lookup-failure=false --authorization-kubeconfig=/system/secrets/kubernetes/kube-scheduler/kubeconfig --bind-address=127.0.0.1 --config=/system/config/kubernetes/kube-scheduler/scheduler-config.yaml --leader-elect=true --profiling=false --tls-min-version=VersionTLS13
root 2902 0.0 0.1 1237888 14808 ? Sl Jun26 0:22 /bin/containerd-shim-runc-v2 -namespace k8s.io -id 59c08140b68ec00161956250e6235588cbbf068b359dd4aa623f9a7d6e75d042 -address /run/containerd/containerd.sock
65535 2922 0.0 0.0 996 640 ? Ss Jun26 0:00 \_ /pause
root 2950 0.0 0.6 1284764 52140 ? Ssl Jun26 0:22 \_ /usr/local/bin/kube-proxy --cluster-cidr=10.244.0.0/16 --conntrack-max-per-core=0 --hostname-override=k8s-talos-cp01 --kubeconfig=/etc/kubernetes/kubeconfig --proxy-mode=iptables
root 3174 0.0 0.1 1238400 15700 ? Sl Jun26 0:49 /bin/containerd-shim-runc-v2 -namespace k8s.io -id
...
また、Nodeのマウントテーブルを確認してみます。
単純にmount
コマンドを実行するとデバッグPodのマウントテーブルを参照してしまうため、ここでは/proc
を経由してNodeのinitプロセスのマウントテーブルから間接的に参照します。
root@k8s-talos-cp01:/# cat /host/proc/1/mounts
devtmpfs /dev devtmpfs rw,nosuid,relatime,size=4021728k,nr_inodes=1005432,mode=755 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,relatime 0 0
tmpfs /run tmpfs rw,nosuid,noexec,relatime,mode=755 0 0
tmpfs /system tmpfs rw,relatime,mode=755 0 0
tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k,mode=755 0 0
/dev/loop0 / squashfs ro,relatime,errors=continue 0 0
...
/dev/sda5 /system/state xfs rw,noatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0
/dev/sda6 /var xfs rw,noatime,attr2,inode64,logbufs=8,logbsize=32k,prjquota 0 0
...
この結果から、Talos Linuxの特徴で解説した通り、rootfsがRead-Onlyでマウントされていることが確認できます。
ただし/system/state
や/var
についてはrw
になっているようです。
/system/state
配下には、先ほど解説した通りtalosctl apply-config
で適用したmachine config
などがファイルとして永続化されています。
また、/var
配下にはコンテナ内でファイルの作成や更新を行った場合の変更差分が格納されるため、このようになっている(せざるを得ない)と思われます。
procfsやsysfsについても、同じくrw
が設定されています。
コンテナブレイクアウトの検証
コンテナからコンテナホストに侵入したり干渉する行為は、コンテナブレイクアウトやコンテナエクスプロイトと呼ばれます。 言うまでもありませんが、攻撃者がコンテナホストを操作できるのは、非常に危険な状態です。
Talos Linuxの特徴で解説した通り、Talos Linuxはコンテナを動作させるのに必要最小限のものだけを含めるなど、セキュリティを意識した作りになっています。
この効果を確認するために、簡単な実験をしてみます。
まずは次のPodのマニフェストを用意します。
このマニフェストでは、特権コンテナを含むPodを定義しています。
apiVersion: v1
kind: Pod
metadata:
name: ubuntu-privileged
labels:
app: ubuntu
spec:
hostPID: true
containers:
- name: ubuntu
image: ubuntu:22.04
command: ["/bin/sleep", "infinity"]
securityContext:
privileged: true
terminationGracePeriodSeconds: 0
通常のKubernetesクラスタにこのマニフェストを用いてPodをデプロイすると、次のようにPod内のコンテナからNodeに侵入することが可能です。(悪用はしないでください。)
# Kubernetesクラスタを構成するNodeを確認
$ kubectl get node
NAME STATUS ROLES AGE VERSION
k8s-cluster1-cp01 Ready control-plane 16d v1.30.2
k8s-cluster1-node01 Ready <none> 16d v1.30.2
k8s-cluster1-node02 Ready <none> 16d v1.30.2
k8s-cluster1-node03 Ready <none> 16d v1.30.2
# Podをデプロイ
$ kubectl apply -f ubuntu-privileged.yaml
# Podのデプロイ先Nodeを確認(今回はk8s-cluster1-node03)
$ kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ubuntu-privileged 1/1 Running 0 17s 10.0.1.2 k8s-cluster1-node03 <none> <none>
# デプロイしたPodに接続
$ kubectl exec -it ubuntu-privileged -- /bin/bash
# ホスト名を確認(Pod名が表示される)
root@ubuntu-privileged:/# hostname
ubuntu-privileged
# Nodeに侵入するコマンドを実行
root@ubuntu-privileged:/# nsenter -t 1 -a /bin/bash
# ホスト名を確認(Nodeのホスト名が表示される)
root@k8s-cluster1-node03:/# hostname
k8s-cluster1-node03
# lsコマンドを実行するとNode(k8s-cluster1-node03)の/配下のファイルやディレクトリが表示される
root@k8s-cluster1-node03:/# ls
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv swap.img sys tmp usr var
# Node(k8s-cluster1-node03)の/配下にファイルを作成
root@k8s-cluster1-node03:/# touch testfile
# ファイルが作成されたことの確認
root@k8s-cluster1-node03:/# ls
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv swap.img sys testfile tmp usr var
続いて、今回Talos Linuxを用いて構築したKubernetesクラスタについても同じ実験をしてみます。
因みにデフォルトの状態ではPodSecurity Admissionの設定が有効になっているため、そもそも今回のような特権コンテナを含むPodはTalos Linuxを用いて構築したKubernetesクラスタにデプロイすることはできません。
# Kubernetesクラスタを構成するNodeを確認
$ kubectl --kubeconfig ./alternative-kubeconfig get node
NAME STATUS ROLES AGE VERSION
k8s-talos-cp01 Ready control-plane 2d22h v1.30.1
k8s-talos-node01 Ready <none> 2d22h v1.30.1
k8s-talos-node02 Ready <none> 2d22h v1.30.1
# Podをデプロイ
$ kubectl --kubeconfig ./alternative-kubeconfig apply -f ubuntu-privileged.yaml
# Podのデプロイ先Nodeを確認(今回はk8s-talos-node01)
$ kubectl --kubeconfig ./alternative-kubeconfig get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-debugger-k8s-talos-cp01-mtj6k 1/1 Running 0 132m 192.168.2.140 k8s-talos-cp01 <none> <none>
ubuntu-privileged 1/1 Running 0 26s 10.244.106.66 k8s-talos-node01 <none> <none>
# デプロイしたPodに接続
$ kubectl --kubeconfig ./alternative-kubeconfig exec -it ubuntu-privileged -- /bin/bash
# ホスト名を確認(Pod名が表示される)
root@ubuntu-privileged:/# hostname
ubuntu-privileged
# Nodeに侵入するコマンドを実行(失敗)
root@ubuntu-privileged:/# nsenter -t 1 -a /bin/bash
nsenter: failed to execute /bin/bash: No such file or directory
上記のように、Pod内のコンテナからNodeへの侵入に失敗したことが確認できます。
Nodeへの侵入に用いたnsenter
コマンドでは、Nodeに含まれる任意のコマンド(今回の場合は/bin/bash
)をNode上で実行することで、コンテナからNodeへの侵入を実現しています。しかしTalos Linuxにはそもそも/bin/bash
が含まれていないため、このようにエラーとなり侵入に失敗したことになります。
この他にも、似たようなタイプのコンテナブレイクアウトの手法として、例えば次のようなものがあります。
また、今回はPodの設定にて.spec.hostPID: true
という設定を行っているため、先ほどのデバッグPodと同様にコンテナ内からNodeのプロセスを参照することができます。
これを利用すると/proc/1/root
ディレクトリを経由して、Nodeの/配下を参照することができます。
root@ubuntu-privileged:/# ls /proc/1/root
bin boot dev etc lib mnt opt proc root run sbin sys system tmp usr var
ここに対してファイルの作成を行うことができるか確認します。
root@ubuntu-privileged:/# touch /proc/1/root/testfile
touch: cannot touch '/proc/1/root/testfile': Read-only file system
先ほど確認したようにTalos LinuxではrootfsがRead-Onlyでマウントされているため、ファイルの作成に失敗します。
このように、
- コンテナを動作させるのに必要最小限のものだけを含んでいる
- rootfsがRead-Onlyでマウントされている
というTalos Linuxの特徴が、セキュリティの観点から効果を発揮していることが確認できます。
ただし「コンテナはコンテナホストのカーネルを共有している」という特性に起因する攻撃には、対処できない点に注意が必要です。
例えば、LinuxカーネルにはMagic System Request Keyという機能があります。
この機能を利用すると、カーネルパニックを起こしたりシャットダウンなど、Linuxカーネルに対して様々な操作を行うことができます。
Magic System Request Keyはコンテナにrwとしてマウントされているprocfsを経由して利用することができるため、例えばPod内のコンテナから次のコマンドを実行するとNodeがシャットダウンされます。
root@ubuntu-privileged:/# echo o > /proc/sysrq-trigger
$ kubectl --kubeconfig ./alternative-kubeconfig get node
NAME STATUS ROLES AGE VERSION
k8s-talos-cp01 Ready control-plane 2d23h v1.30.1
k8s-talos-node01 NotReady <none> 2d23h v1.30.1
k8s-talos-node02 Ready <none> 2d23h v1.30.1
このように、いくらTalos Linuxが一定のセキュリティ対策が施されたLinuxディストリビューションであるとはいえ、それだけでは防ぎきれない(Kubernetesやコンテナ等のレイヤで対策が必要)なケースもあることは理解しておく必要があります。
まとめ
コンテナ専用OSの1つであるTalos Linuxを用いたKubernetesクラスタの構築と検証を実施しましたが、サーバー自体の操作をAPIに限定したり、徹底した軽量化やimmutable化が図られていたりと、セキュリティへの意識をとても強く感じるディストリビューションでした。
また、サーバーの設定にOperator Patternが用いられている点も、普段Kubernetesに触れている身としては大変興味深く思いました。
Discussion