本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

非 root 容器和裝置

當用戶希望在 Linux 上部署使用加速器裝置(透過 Kubernetes 裝置外掛)的容器時,Pod 的 securityContext 中與使用者/組 ID 相關的安全設定會引發問題。在這篇博文中,我將討論這個問題並描述迄今為止為解決它所做的工作。這不是一個關於如何修復 k/k 問題的長篇故事。

相反,本文旨在提高對這個問題的認識,並強調重要的裝置用例。這是必要的,因為 Kubernetes 正在開發新的相關功能,例如對使用者名稱空間的支援。

為什麼非 root 容器不能使用裝置,以及為什麼這很重要

在 Kubernetes 中執行容器的關鍵安全原則之一是最小許可權原則。Pod/容器的 securityContext 指定了要設定的配置選項,例如 Linux 能力、MAC 策略以及使用者/組 ID 值來實現此目的。

此外,叢集管理員可以使用 PodSecurityPolicy(已棄用)或 Pod Security Admission(Alpha)等工具來強制執行部署在叢集中的 Pod 所需的安全設定。這些設定可能要求容器必須 runAsNonRoot,或者禁止它們以 root 的組 ID 執行在 runAsGroupsupplementalGroups 中。

在 Kubernetes 中,kubelet 構建要提供給容器的 Device 資源列表(基於裝置外掛的輸入),並將該列表包含在傳送到 CRI 容器執行時的 CreateContainer CRI 訊息中。每個 Device 包含少量資訊:主機/容器裝置路徑和所需的裝置 cgroup 許可權。

OCI 執行時 Linux 容器配置規範 期望除了裝置 cgroup 欄位外,還必須提供有關裝置的更詳細資訊。

{
        "type": "<string>",
        "path": "<string>",
        "major": <int64>,
        "minor": <int64>,
        "fileMode": <uint32>,
        "uid": <uint32>,
        "gid": <uint32>
},

CRI 容器執行時(containerd、CRI-O)負責從主機獲取每個 Device 的此資訊。預設情況下,執行時會複製主機裝置的`uid`和`gid`。

  • uid (uint32, 可選) - 容器名稱空間中裝置所有者的 ID。
  • gid (uint32, 可選) - 容器名稱空間中裝置組的 ID。

同樣,執行時會根據 CRI 欄位(包括 securityContext 中定義的欄位:runAsUser/runAsGroup)準備其他強制性的 config.json 部分,這些欄位透過以下方式成為 POSIX 平臺使用者結構的一部分:

  • uid (int, 必填) 指定容器名稱空間中的使用者 ID。
  • gid (int, 必填) 指定容器名稱空間中的組 ID。
  • additionalGids (int 陣列, 可選) 指定容器名稱空間中要新增到程序的其他組 ID。

然而,當嘗試運行同時添加了裝置並透過 runAsUser/runAsGroup 設定了非 root uid/gid 的容器時,生成的 config.json 會引發問題:即使容器的組 ID(gid,從主機複製)對非 root 組具有許可許可權,容器使用者程序也無權使用該裝置。這是因為容器使用者不屬於該主機組(例如,透過 additionalGids)。

能夠以非 root 使用者身份執行使用裝置的應用程式是正常且預期可行的,這樣才能滿足安全原則。因此,我們考慮了幾種替代方案來填補 PodSec/CRI/OCI 目前支援的功能中的空白。

為解決該問題做了哪些工作?

您可能已經從問題定義中注意到,至少可以透過手動將裝置 gid 新增到 supplementalGroups 來解決問題,或者在只有一個裝置的情況下,將 runAsGroup 設定為裝置的組 ID。然而,這存在問題,因為裝置 gid 的值可能因叢集中節點的發行版/版本而異。例如,對於 GPU,不同發行版和版本的以下命令會返回不同的 gid:

Fedora 33

$ ls -l /dev/dri/
total 0
drwxr-xr-x. 2 root root         80 19.10. 10:21 by-path
crw-rw----+ 1 root video  226,   0 19.10. 10:42 card0
crw-rw-rw-. 1 root render 226, 128 19.10. 10:21 renderD128
$ grep -e video -e render /etc/group
video:x:39:
render:x:997:

Ubuntu 20.04

$ ls -l /dev/dri/
total 0
drwxr-xr-x 2 root root         80 19.10. 17:36 by-path
crw-rw---- 1 root video  226,   0 19.10. 17:36 card0
crw-rw---- 1 root render 226, 128 19.10. 17:36 renderD128
$ grep -e video -e render /etc/group
video:x:44:
render:x:133:

在您的 securityContext 中選擇哪個數字?此外,如果 runAsGroup/runAsUser 值不能硬編碼,因為它們在 Pod 准入期間透過外部安全策略自動分配怎麼辦?

與帶有 fsGroup 的卷不同,裝置沒有 deviceGroup/deviceUser 的官方概念,CRI 執行時(或 kubelet)無法使用。我們考慮使用裝置外掛設定的容器註解(例如,io.kubernetes.cri.hostDeviceSupplementalGroup/)來獲取自定義 OCI config.json 的 uid/gid 值。這將需要更改所有現有的裝置外掛,這並不理想。

相反,我們更傾向於一種對終端使用者**無縫**的解決方案,無需裝置外掛供應商參與。所選擇的方法是重用 config.json 中用於裝置的 runAsUserrunAsGroup 值。

{
        "type": "c",
        "path": "/dev/foo",
        "major": 123,
        "minor": 4,
        "fileMode": 438,
        "uid": <runAsUser>,
        "gid": <runAsGroup>
},

使用 runc OCI 執行時(在非 rootless 模式下),裝置在容器名稱空間中建立(mknod(2)),並使用 chmod(2) 將所有權更改為 runAsUser/runAsGroup

在容器名稱空間中更新所有權是合理的,因為使用者程序是唯一訪問裝置的使用者。僅考慮 runAsUser/runAsGroup,例如,容器中的 USER 設定目前被忽略。

雖然“有問題的”部署(即非 root securityContext + 裝置)可能不存在,但為了確保沒有任何部署中斷,我們在 containerd 和 CRI-O 中都添加了一個選擇性配置條目來啟用新行為。以下配置

device_ownership_from_security_context (布林值)

預設為 false,必須啟用才能使用該功能。

修復後檢視使用裝置的非 root 容器

為了演示新行為,我們以使用硬體加速器、Kubernetes CPU 管理器和 HugePages 的資料平面開發工具包 (DPDK) 應用程式為例。叢集執行 containerd,配置如下:

[plugins]
  [plugins."io.containerd.grpc.v1.cri"]
    device_ownership_from_security_context = true

或 CRI-O 配置如下:

[crio.runtime]
device_ownership_from_security_context = true

並使用以下 YAML 執行 DPDK 的 crypto-perf 測試工具的 Guaranteed QoS Class Pod:

...
metadata:
  name: qat-dpdk
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 2000
    fsGroup: 3000
  containers:
  - name: crypto-perf
    image: intel/crypto-perf:devel
    ...
    resources:
      requests:
        cpu: "3"
        memory: "128Mi"
        qat.intel.com/generic: '4'
        hugepages-2Mi: "128Mi"
      limits:
        cpu: "3"
        memory: "128Mi"
        qat.intel.com/generic: '4'
        hugepages-2Mi: "128Mi"
  ...

要驗證結果,請檢查容器執行的使用者和組 ID:

$ kubectl exec -it qat-dpdk -c crypto-perf -- id

它們被設定為非零值,符合預期

uid=1000 gid=2000 groups=2000,3000

接下來,檢查裝置節點許可權 (qat.intel.com/generic 暴露 /dev/vfio/ 裝置) 是否可供 runAsUser/runAsGroup 訪問。

$ kubectl exec -it qat-dpdk -c crypto-perf -- ls -la /dev/vfio
total 0
drwxr-xr-x 2 root root      140 Sep  7 10:55 .
drwxr-xr-x 7 root root      380 Sep  7 10:55 ..
crw------- 1 1000 2000 241,   0 Sep  7 10:55 58
crw------- 1 1000 2000 241,   2 Sep  7 10:55 60
crw------- 1 1000 2000 241,  10 Sep  7 10:55 68
crw------- 1 1000 2000 241,  11 Sep  7 10:55 69
crw-rw-rw- 1 1000 2000  10, 196 Sep  7 10:55 vfio

最後,檢查非 root 容器是否也允許建立 HugePages。

$ kubectl exec -it qat-dpdk -c crypto-perf -- ls -la /dev/hugepages/

fsGrouprunAsUser 提供可寫 HugePages 的 emptyDir 掛載點。

total 0
drwxrwsr-x 2 root 3000   0 Sep  7 10:55 .
drwxr-xr-x 7 root root 380 Sep  7 10:55 ..

幫助我們測試並提供反饋!

這裡描述的功能有望幫助提高叢集安全性和裝置許可權的可配置性。為了允許非 root 容器使用裝置,叢集管理員需要透過設定 device_ownership_from_security_context = true 來選擇啟用該功能。要使其成為預設設定,請測試並提供您的反饋(透過 SIG-Node 會議或問題)!該標誌已在 CRI-O v1.22 版本中提供,並已排隊等待 containerd v1.6。

還需要更多工作才能**正確**支援它。已知它與 runc 配合使用,但它也需要與其他 OCI 執行時(如果適用)一起使用。例如,Kata Containers 支援裝置直通,並允許將裝置提供給虛擬機器沙箱中的容器。

此外,支援使用者名稱空間和裝置帶來了額外的挑戰。這個問題仍然懸而未決,需要更多的集思廣益。

最後,需要了解 runAsUser/runAsGroup 是否足夠,或者 PodSpec/CRI v2 中是否需要類似於 fsGroups 的裝置特定設定。

感謝

我感謝 Mike Brown(IBM,containerd)、Peter Hunt(Redhat,CRI-O)和 Alexander Kanevskiy(Intel)提供的所有反饋和愉快的交流。