建一個比較健康的 k8s
帶有些微潔癖的 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 的需求
Comments on Matrix #comments_xdavidwu.link_/kubernetes/healthy-k8s/:xdavidwu.link