CRI-O:從 OCI 登錄檔應用 seccomp 設定檔

Seccomp 代表安全運算模式,自 2.6.12 版以來一直是 Linux 核心的一項功能。它可用於沙箱化程序的權限,限制其從使用者空間向核心發出的呼叫。Kubernetes 可讓您自動將節點上載入的 seccomp 設定檔套用至您的 Pod 和容器。

但分發這些 seccomp 設定檔是 Kubernetes 中的一項主要挑戰,因為 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 設定檔,當不使用容器名稱尾碼但使用保留名稱 POD 時:seccomp-profile.kubernetes.cri-o.io/POD
  • 整個容器映像檔的 seccomp 設定檔,如果映像檔本身包含註釋 seccomp-profile.kubernetes.cri-o.io/PODseccomp-profile.kubernetes.cri-o.io/<CONTAINER>

CRI-O 僅在執行期配置為允許註釋時才會尊重該註釋,以及對於以 Unconfined 方式運行的工作負載。所有其他工作負載仍將使用來自 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 陣列。這可以使用類似於以下的嵌入式配置 /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 commit 運行 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。