🦊

コンテナ専用OS Talos Linuxを用いたKubernetesクラスタ(検証編)

2024/07/03に公開

この記事の概要

本記事は以下記事の続きです。
Talos Linuxを用いて構築したKubernetesクラスタに対して、いくつか検証を行います。

https://zenn.dev/mochizuki875/articles/4453b2ce113bd2

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 PlaneNodeについてはユーザーからリクエストを受け付ける必要はないため、ユーザーから直接アクセスできないネットワークに配置されるのが一般的です。
このようなケースでも、--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 edittalosctl patchといったコマンドを利用することもできます。)
例えばサーバーの/etc/hostsにレコードを追記したい場合は、次のように.network.extraHostEntriesにレコードを追加したmachine configを作成します。(ここではcontrolplane.yamlを例にしています。)

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)

この中でも中心的な役割を担っているのがapidmachinedという2つのコンポーネントです。
apidはTalos Linuxを操作するためのAPIを公開しており、talosctlコマンドを実行した際もここにリクエストが送信されます。
また、EndpointとNodeで解説したAPIリクエストを自分以外のサーバーに転送する役割も有しています。
因みにapidはTalos Linux上でコンテナとして実行されています。(ただしKubernetesからはPodとして確認することができません。)

machinedはTalos Linuxのinitプロセスとして動作し、内部ではさまざまなControllerが実行されているようです。

https://www.talos.dev/v1.7/learn-more/controllers-resources/

先ほど解説した通り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-apiserverkube-controller-managerkube-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に接続したかのような状態を作り出すことができます。

https://kubernetes.io/docs/tasks/debug/debug-cluster/kubectl-node-debug/

詳細な仕組みは割愛しますが、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のコンポーネントであるapidtrustdがコンテナとして実行されているのに加え、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として実行するのが一般的)

https://www.talos.dev/v1.7/learn-more/components/#containerd

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が設定されています。

https://www.talos.dev/v1.7/learn-more/architecture/#the-file-system

コンテナブレイクアウトの検証

コンテナからコンテナホストに侵入したり干渉する行為は、コンテナブレイクアウトやコンテナエクスプロイトと呼ばれます。 言うまでもありませんが、攻撃者がコンテナホストを操作できるのは、非常に危険な状態です。
Talos Linuxの特徴で解説した通り、Talos Linuxはコンテナを動作させるのに必要最小限のものだけを含めるなど、セキュリティを意識した作りになっています。
この効果を確認するために、簡単な実験をしてみます。

まずは次のPodのマニフェストを用意します。
このマニフェストでは、特権コンテナを含むPodを定義しています。

ubuntu-privileged.yaml
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が含まれていないため、このようにエラーとなり侵入に失敗したことになります。

この他にも、似たようなタイプのコンテナブレイクアウトの手法として、例えば次のようなものがあります。

https://container-security.dev/security/breakout-to-host.html

また、今回は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という機能があります。

https://www.kernel.org/doc/html/v4.10/admin-guide/sysrq.html

この機能を利用すると、カーネルパニックを起こしたりシャットダウンなど、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