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

CRI-O:從 OCI 映象庫應用 seccomp 配置檔案

Seccomp 是 secure computing mode(安全計算模式)的縮寫,自 Linux 核心 2.6.12 版本以來一直是其一項特性。它可用於對程序的許可權進行沙箱化,限制該程序能夠從使用者空間向核心發起的呼叫。Kubernetes 允許你將節點上載入的 seccomp 配置檔案自動應用到你的 Pod 和容器中。

但在 Kubernetes 中分發這些 seccomp 配置檔案是一個主要挑戰,因為 JSON 檔案必須在工作負載可能執行的所有節點上都可用。像 Security Profiles Operator 這樣的專案透過在叢集內作為守護程序執行來解決這個問題,這讓我思考分發的哪個部分可以由容器執行時來完成。

執行時通常從本地路徑應用配置檔案,例如

apiVersion: v1
kind: Pod
metadata:
  name: pod
spec:
  containers:
    - name: container
      image: nginx:1.25.3
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: nginx-1.25.3.json

配置檔案 nginx-1.25.3.json 必須在 kubelet 的根目錄中可用,並附加 seccomp 目錄。這意味著該配置檔案在磁碟上的預設位置是 /var/lib/kubelet/seccomp/nginx-1.25.3.json。如果配置檔案不可用,則執行時在建立容器時會失敗,如下所示

kubectl get pods
NAME   READY   STATUS                 RESTARTS   AGE
pod    0/1     CreateContainerError   0          38s
kubectl describe pod/pod | tail
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason     Age                 From               Message
  ----     ------     ----                ----               -------
  Normal   Scheduled  117s                default-scheduler  Successfully assigned default/pod to 127.0.0.1
  Normal   Pulling    117s                kubelet            Pulling image "nginx:1.25.3"
  Normal   Pulled     111s                kubelet            Successfully pulled image "nginx:1.25.3" in 5.948s (5.948s including waiting)
  Warning  Failed     7s (x10 over 111s)  kubelet            Error: setup seccomp: unable to load local profile "/var/lib/kubelet/seccomp/nginx-1.25.3.json": open /var/lib/kubelet/seccomp/nginx-1.25.3.json: no such file or directory
  Normal   Pulled     7s (x9 over 111s)   kubelet            Container image "nginx:1.25.3" already present on machine

手動分發 Localhost 配置檔案的主要障礙將導致許多終端使用者退回到使用 RuntimeDefault,甚至以 Unconfined(停用 seccomp)方式執行其工作負載。

CRI-O 來拯救

Kubernetes 容器執行時 CRI-O 使用自定義註解提供了多種功能。v1.30 版本 添加了對一組新註解的支援,名為 seccomp-profile.kubernetes.cri-o.io/PODseccomp-profile.kubernetes.cri-o.io/<CONTAINER>。這些註解允許你指定

  • 用於特定容器的 seccomp 配置檔案,使用方式為:seccomp-profile.kubernetes.cri-o.io/<CONTAINER>(例如:seccomp-profile.kubernetes.cri-o.io/webserver: 'registry.example/example/webserver:v1'
  • 用於 Pod 內每個容器的 seccomp 配置檔案,使用時沒有容器名稱字尾,而是使用保留名稱 PODseccomp-profile.kubernetes.cri-o.io/POD
  • 用於整個容器映象的 seccomp 配置檔案,如果映象本身包含註解 seccomp-profile.kubernetes.cri-o.io/PODseccomp-profile.kubernetes.cri-o.io/<CONTAINER>

只有當執行時配置為允許該註解,並且工作負載以 Unconfined 模式執行時,CRI-O 才會遵守該註解。所有其他工作負載仍將使用 securityContext 中的值,且優先順序更高。

僅靠註解對分發配置檔案幫助不大,但引用它們的方式卻可以!例如,你現在可以透過使用 OCI 製品來像指定常規容器映象一樣指定 seccomp 配置檔案

apiVersion: v1
kind: Pod
metadata:
  name: pod
  annotations:
    seccomp-profile.kubernetes.cri-o.io/POD: quay.io/crio/seccomp:v2
spec: 

映象 quay.io/crio/seccomp:v2 包含一個 seccomp.json 檔案,其中包含實際的配置檔案內容。可以使用 ORASSkopeo 等工具來檢查映象的內容

oras pull quay.io/crio/seccomp:v2
Downloading 92d8ebfa89aa seccomp.json
Downloaded  92d8ebfa89aa seccomp.json
Pulled [registry] quay.io/crio/seccomp:v2
Digest: sha256:f0205dac8a24394d9ddf4e48c7ac201ca7dcfea4c554f7ca27777a7f8c43ec1b
jq . seccomp.json | head
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 38,
  "defaultErrno": "ENOSYS",
  "archMap": [
    {
      "architecture": "SCMP_ARCH_X86_64",
      "subArchitectures": [
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
# Inspect the plain manifest of the image
skopeo inspect --raw docker://quay.io/crio/seccomp:v2 | jq .
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config":
    {
      "mediaType": "application/vnd.cncf.seccomp-profile.config.v1+json",
      "digest": "sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
      "size": 3,
    },
  "layers":
    [
      {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",
        "digest": "sha256:92d8ebfa89aa6dd752c6443c27e412df1b568d62b4af129494d7364802b2d476",
        "size": 18853,
        "annotations": { "org.opencontainers.image.title": "seccomp.json" },
      },
    ],
  "annotations": { "org.opencontainers.image.created": "2024-02-26T09:03:30Z" },
}

映象清單包含一個對特定必需的配置媒體型別(application/vnd.cncf.seccomp-profile.config.v1+json)的引用,以及一個指向 seccomp.json 檔案的單一層(application/vnd.oci.image.layer.v1.tar)。現在,讓我們來試試這個新功能!

為特定容器或整個 pod 使用註解

CRI-O 需要進行適當配置才能使用該註解。為此,將該註解新增到執行時的 allowed_annotations 陣列中。這可以透過使用一個 drop-in 配置 /etc/crio/crio.conf.d/10-crun.conf 來完成,如下所示

[crio.runtime]
default_runtime = "crun"

[crio.runtime.runtimes.crun]
allowed_annotations = [
    "seccomp-profile.kubernetes.cri-o.io",
]

現在,讓我們從最新的 main 提交執行 CRI-O。這可以透過從原始碼構建、使用靜態二進位制包預釋出包來完成。

為了演示,我透過 local-up-cluster.sh 在一個單節點 Kubernetes 叢集上從命令列運行了 crio 二進位制檔案。現在叢集已經啟動並執行,讓我們嘗試一個沒有註解、以 seccomp Unconfined 模式執行的 pod

cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod
spec:
  containers:
    - name: container
      image: nginx:1.25.3
      securityContext:
        seccompProfile:
          type: Unconfined
kubectl apply -f pod.yaml

工作負載已啟動並正在執行

kubectl get pods
NAME   READY   STATUS    RESTARTS   AGE
pod    1/1     Running   0          15s

如果我使用 crictl 檢查容器,會發現沒有應用 seccomp 配置檔案

export CONTAINER_ID=$(sudo crictl ps --name container -q)
sudo crictl inspect $CONTAINER_ID | jq .info.runtimeSpec.linux.seccomp
null

現在,讓我們修改 pod,將配置檔案 quay.io/crio/seccomp:v2 應用到容器中

apiVersion: v1
kind: Pod
metadata:
  name: pod
  annotations:
    seccomp-profile.kubernetes.cri-o.io/container: quay.io/crio/seccomp:v2
spec:
  containers:
    - name: container
      image: nginx:1.25.3

我必須刪除並重新建立 Pod,因為只有重新建立才會應用新的 seccomp 配置檔案

kubectl delete pod/pod
pod "pod" deleted
kubectl apply -f pod.yaml
pod/pod created

CRI-O 的日誌現在會顯示執行時已拉取了該製品

WARN[…] Allowed annotations are specified for workload [seccomp-profile.kubernetes.cri-o.io]
INFO[…] Found container specific seccomp profile annotation: seccomp-profile.kubernetes.cri-o.io/container=quay.io/crio/seccomp:v2  id=26ddcbe6-6efe-414a-88fd-b1ca91979e93 name=/runtime.v1.RuntimeService/CreateContainer
INFO[…] Pulling OCI artifact from ref: quay.io/crio/seccomp:v2  id=26ddcbe6-6efe-414a-88fd-b1ca91979e93 name=/runtime.v1.RuntimeService/CreateContainer
INFO[…] Retrieved OCI artifact seccomp profile of len: 18853  id=26ddcbe6-6efe-414a-88fd-b1ca91979e93 name=/runtime.v1.RuntimeService/CreateContainer

容器最終使用了該配置檔案

export CONTAINER_ID=$(sudo crictl ps --name container -q)
sudo crictl inspect $CONTAINER_ID | jq .info.runtimeSpec.linux.seccomp | head
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 38,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {

如果使用者將 /container 字尾替換為保留名稱 /POD,那麼同樣的效果也適用於 pod 中的每個容器,例如

apiVersion: v1
kind: Pod
metadata:
  name: pod
  annotations:
    seccomp-profile.kubernetes.cri-o.io/POD: quay.io/crio/seccomp:v2
spec:
  containers:
    - name: container
      image: nginx:1.25.3

為容器映象使用註解

雖然將 seccomp 配置檔案作為 OCI 製品指定給某些工作負載是一項很酷的功能,但大多數終端使用者希望將 seccomp 配置檔案連結到已釋出的容器映象。這可以透過使用容器映象註解來完成;該註解不是應用於 Kubernetes Pod,而是在容器映象本身應用的元資料。例如,可以使用 Podman 在映象構建期間直接新增映象註解

podman build \
    --annotation seccomp-profile.kubernetes.cri-o.io=quay.io/crio/seccomp:v2 \
    -t quay.io/crio/nginx-seccomp:v2 .

推送的映象隨後會包含該註解

skopeo inspect --raw docker://quay.io/crio/nginx-seccomp:v2 |
    jq '.annotations."seccomp-profile.kubernetes.cri-o.io"'
"quay.io/crio/seccomp:v2"

如果我現在在 CRI-O 測試 pod 定義中使用該映象

apiVersion: v1
kind: Pod
metadata:
  name: pod
  # no Pod annotations set
spec:
  containers:
    - name: container
      image: quay.io/crio/nginx-seccomp:v2

那麼 CRI-O 的日誌將表明映象註解已被評估,並且配置檔案已應用

kubectl delete pod/pod
pod "pod" deleted
kubectl apply -f pod.yaml
pod/pod created
INFO[…] Found image specific seccomp profile annotation: seccomp-profile.kubernetes.cri-o.io=quay.io/crio/seccomp:v2  id=c1f22c59-e30e-4046-931d-a0c0fdc2c8b7 name=/runtime.v1.RuntimeService/CreateContainer
INFO[…] Pulling OCI artifact from ref: quay.io/crio/seccomp:v2  id=c1f22c59-e30e-4046-931d-a0c0fdc2c8b7 name=/runtime.v1.RuntimeService/CreateContainer
INFO[…] Retrieved OCI artifact seccomp profile of len: 18853  id=c1f22c59-e30e-4046-931d-a0c0fdc2c8b7 name=/runtime.v1.RuntimeService/CreateContainer
INFO[…] Created container 116a316cd9a11fe861dd04c43b94f45046d1ff37e2ed05a4e4194fcaab29ee63: default/pod/container  id=c1f22c59-e30e-4046-931d-a0c0fdc2c8b7 name=/runtime.v1.RuntimeService/CreateContainer
export CONTAINER_ID=$(sudo crictl ps --name container -q)
sudo crictl inspect $CONTAINER_ID | jq .info.runtimeSpec.linux.seccomp | head
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 38,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {

對於容器映象,註解 seccomp-profile.kubernetes.cri-o.io 將被視為與 seccomp-profile.kubernetes.cri-o.io/POD 相同,並應用於整個 pod。除此之外,如果對映象使用特定於容器的註解,整個功能也同樣有效,例如,如果一個容器名為 container1

skopeo inspect --raw docker://quay.io/crio/nginx-seccomp:v2-container |
    jq '.annotations."seccomp-profile.kubernetes.cri-o.io/container1"'
"quay.io/crio/seccomp:v2"

這個功能的妙處在於,使用者現在可以為特定的容器映象建立 seccomp 配置檔案,並將它們並排儲存在同一個映象倉庫中。將映象與配置檔案連結起來,為在整個應用程式生命週期中維護它們提供了極大的靈活性。

使用 ORAS 推送配置檔案

當使用 ORAS 時,實際建立包含 seccomp 配置檔案的 OCI 物件需要更多的工作。我希望像 Podman 這樣的工具將來能簡化整個過程。目前,容器映象倉庫需要是OCI 相容的Quay.io 也是如此。CRI-O 期望 seccomp 配置檔案物件具有容器映象媒體型別(application/vnd.cncf.seccomp-profile.config.v1+json),而 ORAS 預設使用 application/vnd.oci.empty.v1+json。為了實現這一切,可以執行以下命令

echo "{}" > config.json
oras push \
    --config config.json:application/vnd.cncf.seccomp-profile.config.v1+json \
     quay.io/crio/seccomp:v2 seccomp.json

生成的映象包含了 CRI-O 所期望的 mediaType。ORAS 將一個名為 seccomp.json 的單層推送到映象倉庫。配置檔案的名稱並不重要。CRI-O 會選擇第一個層並檢查其是否可以作為 seccomp 配置檔案。

未來的工作

CRI-O 內部像管理常規檔案一樣管理 OCI 製品。這帶來了移動它們、在不再使用時移除它們或擁有除 seccomp 配置檔案之外的任何其他資料的好處。這為 CRI-O 未來基於 OCI 製品的增強功能提供了可能,也讓我們能夠思考將 seccomp 配置檔案作為 OCI 製品中多個層的一部分進行堆疊。v1.30.x 版本中它僅適用於 Unconfined 工作負載的限制是 CRI-O 希望在未來解決的問題。在不犧牲安全性的前提下簡化整體使用者體驗,似乎是 seccomp 在容器工作負載中成功未來的關鍵。

CRI-O 的維護者們很樂意傾聽關於這個新功能的任何反饋或建議!感謝您閱讀這篇部落格文章,歡迎透過 Kubernetes 的 Slack 頻道 #crio 聯絡維護者,或在 GitHub 倉庫中建立 issue。