建一個比較健康的 k8s

9 minute read

帶有些微潔癖的 k8s 折騰紀錄

目標:

  • single control-pane
  • user namespacing
  • CRI-O + crun

環境:

  • master 和其餘 nodes 在同一個內網
  • master 在 gateway 上
  • 所有 nodes 皆有現有 bridges
  • 使用既有內網

成果:

  • kube-apiserver, kube-controller-manager, kube-scheduler 和所有 pods 跑在 user namespace enabled containers
  • kube-proxy 直接跑在 host

安裝:

裝 crun, cri-o, kubelet, kubectl, kubeadm, 注意版本相容, 記得 package manager 鎖版防手殘造成不預期的更新

我因為安裝時 cri-o 提供的套件只有到 1.17 所以全部鎖 1.17 的 branch

configure cri-oPermalink

/etc/crio/crio.conf:

修改預設 runtime

default_runtime = "crun"

因為 hosts 用了 ubuntu, 設 apparmor

apparmor_profile = "crio-default-1.17.4"

drop 掉一堆可能不用的預設 capabilities

default_capabilities = [
	#"CHOWN", 
	#"DAC_OVERRIDE", 
	#"FSETID", 
	#"FOWNER", 
	#"NET_RAW", 
	#"SETGID", 
	#"SETUID", 
	#"SETPCAP", 
	"NET_BIND_SERVICE",
	#"SYS_CHROOT", 
	#"KILL", 
]

NET_BIND_SERVICE 感覺常用就留了

PID limit 先壓小不夠再改就好

pids_limit = 128

設 UID/GID mappings 啟用 user namespacing

uid_mappings = "0:10000000:65536"
gid_mappings = "0:10000000:65536"

設 10000000 開始是因為我在一些機器上有做 lxc containers 也有 user namespacing 需求, 是手動每個 containers 都錯開分配的, 用量頗多, 所以我抓了一個我覺得夠高不會撞到又好記的 10000000

我 crio 的 user 還是 root, 但其實 crio 有環境變數可以打開 unprivileged mode _CRIO_ROOTLESS, 但如果要配合 user namespacing 就得取消 Debian 加的預防措施 kernel.unprivileged_userns_clone, 考量到 user namespacing 早期跟其他東西配合曾出現不少 privilege escalation, 雖然現在都修了並且瀏覽器的 sandboxing 也都開始利用 unprivileged user namespacing, 有些環境可能還是不怎麼需要開放, 且我的 node 同時含有 k8s 以外的服務, 這裡我選擇留在 privileged user namespacing

注意此 privilege 並非 docker/k8s 指的 privilege

定義 crun runtime

  [crio.runtime.runtimes.crun]

預設會在 $PATH 找跟名字相符的 binary

configure storagePermalink

cri-o 用的 storage

/etc/containers/storage.conf

設定 UID/GID mappings

remap-uids = "0:10000000:65536"
remap-gids = "0:10000000:65536"

configure networkPermalink

我的規劃是利用現有內網跟 bridge, 所以修改預設 example 就行

/etc/cni/net.d/100-crio-bridge.conf

{
    "cniVersion": "0.3.1",
    "name": "crio-bridge",
    "type": "bridge",
    "bridge": "lxcbr0",
    "isGateway": false,
    "ipMasq": false,
    "ipam": {
        "type": "host-local",
        "routes": [
            { "dst": "0.0.0.0/0" }
        ],
        "ranges": [
            [{
                "subnet": "10.0.3.0/24",
                "rangeStart": "10.0.3.16",
                "rangeEnd": "10.0.3.60",
                "gateway": "10.0.3.1"
            }]
        ]
    }
}

根據源碼如果 bridge 存在會沿用

這裡我現有的 bridge 是 lxcbr0, 現有內網是 10.0.3.0/24, 因為我本來這個內網就是手動 allocate address 的, 所以很容易的給每個 node 都留了一段可用空間

configure kubeletPermalink

/etc/default/kubelet

KUBELET_EXTRA_ARGS=--fail-swap-on=false --cgroup-driver=systemd

swap 是個好東西我把他留了, 畢竟通常情況總是會有些 pages 本質上很常 inactive 適合進 swap, 而且 swap 也是個很好的吃緊下的臨時備案

k8s 的 resource 控管都假設沒有 swap, 擔心控管較差的話可以把 swappiness 調低

然後記得 restart kubelet

kubeadmPermalink

bootstrap masterPermalink

sudo kubeadm init --apiserver-advertise-address=10.0.3.1 --ignore-preflight-errors=swap

我 master 同時是內網 gateway, 所以設了 advertise address 避免被從外面戳

會 fail, 因為用了 user namespacing 然而 kube-system 的 pods 都有 bind mount 一些 hosts 端的檔案進去, containers 裡的 root 是 host 端的 UID 10000000

這裡我先創 UID 10000000/GID 10000000 的 passwd/groups entry, 比較好看

sudo useradd -u 10000000 -U k8s
sudo groupmod -g 10000000 k8s

預設應該就有鎖了但保險起見還是 sudo passwd -l k8s 了一下

開始用 group 修權限

sudo chown root:k8s /var/lib/kubelet
sudo chmod g+rx /var/lib/kubelet
sudo chown root:k8s /var/lib/kubelet/pods -R
sudo chmod g+s /var/lib/kubelet/pods

這裡在 directory 用了 set-GID bit, 讓以後創的 pods 套用 group 不用手動再修

修 etcd 用的 directory

sudo chown k8s:k8s -R /var/lib/etcd

修 k8s config

sudo chown k8s:k8s -R /etc/kubernetes

然後再 bootstrap 一次應該就沒問題了

sudo kubeadm init --apiserver-advertise-address=10.0.3.1 --ignore-preflight-errors=all

這裡變成忽略全部檢查 (error => warning), 應該只會多了一堆檔案已經存在跟 port 正在用之類的, 但還是注意一下

kubeadm 如果檔案存在會用現有的

bootstrap nodesPermalink

sudo kubeadm join <token, api server, ca cert...> --ignore-preflight-errors=swap

一樣會 fail

應該只有 /var/lib/kubelet/pods 會需要修

修了後一樣再來一次

sudo kubeadm join <token, api server, ca cert...> --ignore-preflight-errors=all

fix kube-proxyPermalink

到這裡 kubectl 設定好已經能看到 nodes 了, 也可以開 pods 來用

kubectl get pods -n kube-system 會發現 kube-proxy 都起不來, coredns 也都進不了 ready

主要問題來源是 kube-proxy, coredns 只是靠 proxy 戳 api 戳不到

分析 kube-proxy 發現 SecurityContext 有設 privileged, 查詢相似錯誤在 runc 上是 privileged 導致 /dev/tty 在 spec 中被定義, 跟內建 device list 重疊, 然後在 user namespacing 下 device 是透過 bind mount, 重複操作第二次就 fail 了, 懷疑在 crun 上也是相似情況

分析 kube-proxy 本身為何需要 privilege, 發現運作原理會需要修改 host 端 iptables, 測試及讀 man pages 後得知這在 user namespacing 下就算用 capabilities 也因 capabilities 侷限於 namespace 內沒辦法修改到 host 端的 iptables, 觀察 CRI/CRI-O 等層發現當下不易實做根據 pods 關閉 user namespacing, 故放棄使用 k8s container 跑 kube-proxy

kube-proxy 本身實做上除了 iptables 外也會自動幫你 modprobe 和 sysctl, 本身假設的權限就極大, 做 container 保護效果不大

這裡撈出來直接跑在 host 端, 因為我的網路架構往非 10.0.3.0/24 內網都會過 master, 我在 master 上做一個就能達到效果

kubectl get pods <kube-proxy pod on master> -n kube-system -o yaml 觀察各式參數和設定

/var/lib/containers/storage find 出 kube-proxy binary, 把他抓出來裝進 $PATH

/var/lib/kubelet/pods 下找到 kube-proxy pod 的 volumes, 內含他用的 config 和 cert, token 等, 抓出來放好設好權限防護, 例如我放在 /var/lib/kube-proxy, config 內要改的路徑比較少, 注意 cert 和 token 原本在 container 內掛在 /var/run, 這在大多系統上可能是 tmpfs, 改放到一個能保存的地方, 並修改 config 內他們的路徑, 例如我還是一律丟進 /var/lib/kube-proxy, 並且 sudo chmod o= /var/lib/kube-proxy 防止 others 存取

寫個自動 startup, 例如 systemd service

[Unit]
Description=kube-proxy
Wants=network-online.target
Before=multi-user.target

[Service]
Type=simple
ExecStart=/usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf

[Install]
WantedBy=multi-user.target

然後把他 enable 且戳起來

可以開個 pod 戳戳看 dns, 預設是用過 proxy 的 address, 戳的到應該就好了, 可以試試查詢 kubernetes, 會得到過 proxy 的 api server address

刪除原本 kube-proxy 的 DaemonSet

kubectl delete daemonset kube-proxy -n kube-system

這樣基本的 k8s 功能應該都能用了

另外我因為 master node 有對外的 interface, 有掃過 port 並修改各 config 確保沒有不必要的 port 在對外 address 給 listen 到, 有些東西預設是 bind 0.0.0.0 的要注意

這個 setup 的缺點是 privileged container 會撞上面 kube-proxy 最初的問題 fail on create, 就算這點修好了有 user namespacing 下 privileged 也沒有原本那麼多 privilege, 但我們目標本來就是盡量減少 containers 的權限並達到更大的隔離, 我的假設就是沒有 privileged containers 的需求