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

非 Root 容器與裝置

當使用者想要在 Linux 上部署使用加速器裝置的容器(透過 Kubernetes 裝置外掛程式)時,Pod 的 securityContext 中與使用者/群組 ID 相關的安全設定會觸發問題。在這篇部落格文章中,我將討論這個問題,並描述迄今為止為了解決這個問題所做的工作。這並不是要長篇大論關於修復 k/k issue

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

為什麼非 root 容器無法使用裝置以及為什麼這很重要

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

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

在 Kubernetes 中,kubelet 建構要提供給容器的 Device 資源列表(基於來自裝置外掛程式的輸入),並且該列表包含在發送到 CRI 容器運行時的 CreateContainer CRI 訊息中。每個 Device 都包含少量資訊:主機/容器裝置路徑和所需的裝置 cgroups 權限。

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

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

CRI 容器運行時 (containerd、CRI-O) 負責從主機取得每個 Device 的此資訊。預設情況下,運行時會複製主機裝置的使用者和群組 ID

  • uid (uint32, OPTIONAL) - 容器命名空間中裝置擁有者的 ID。
  • gid (uint32, OPTIONAL) - 容器命名空間中裝置群組的 ID。

同樣地,運行時會根據 CRI 欄位準備其他必要的 config.json 區段,包括 securityContext 中定義的欄位:runAsUser/runAsGroup,它們透過以下方式成為 POSIX 平台使用者結構的一部分

  • uid (int, REQUIRED) 指定容器命名空間中的使用者 ID。
  • gid (int, REQUIRED) 指定容器命名空間中的群組 ID。
  • additionalGids (整數陣列, OPTIONAL) 指定要新增到進程的容器命名空間中的其他群組 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 (bool)

預設為 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

以及運行 DPDK 的 crypto-perf 測試工具的 Guaranteed QoS Class Pod,其 YAML 如下

...
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 會議或 issue)!該標誌在 CRI-O v1.22 版本中可用,並已排隊用於 containerd v1.6。

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

此外,額外的挑戰來自於對使用者名稱和裝置的支援。這個問題仍然是 開放的,需要更多的集思廣益。

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

感謝

感謝 Mike Brown (IBM, containerd)、Peter Hunt (Redhat, CRI-O) 和 Alexander Kanevskiy (Intel) 提供的所有回饋和良好的對話。