本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

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。