CRI-O:从 OCI 注册表应用 seccomp 配置文件
Seccomp 代表安全计算模式,自 Linux 内核 2.6.12 版本以来一直是其一项功能。它可以用来沙箱进程的权限,限制其从用户空间到内核的调用。Kubernetes 允许您自动将加载到节点上的 seccomp 配置文件应用到您的 Pod 和容器。
但是,分发这些 seccomp 配置文件是 Kubernetes 中的一个主要挑战,因为 JSON 文件必须在工作负载可能运行的所有节点上可用。像 安全配置文件操作符这样的项目通过在集群中作为守护进程运行来解决这个问题,这让我想到这种分发的哪些部分可以由 容器运行时来完成。
运行时通常从本地路径应用配置文件,例如
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/POD
和 seccomp-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
时,用于 pod 内的每个容器的 seccomp 配置文件:seccomp-profile.kubernetes.cri-o.io/POD
- 如果镜像本身包含注解
seccomp-profile.kubernetes.cri-o.io/POD
或seccomp-profile.kubernetes.cri-o.io/<CONTAINER>
,则为整个容器镜像的 seccomp 配置文件。
只有在配置运行时允许的情况下,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
文件,其中包含实际的配置文件内容。可以使用诸如 ORAS 或 Skopeo 之类的工具来检查镜像的内容
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
提交运行 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 配置文件之外的任何其他可用数据的好处。这使得未来可以在 OCI 工件之上增强 CRI-O,但也允许考虑将 seccomp 配置文件堆叠为 OCI 工件中具有多个层的一部分。它仅适用于 v1.30.x 版本的 Unconfined
工作负载的限制是 CRI-O 未来想要解决的另一个问题。在不损害安全性的前提下简化整体用户体验似乎是 seccomp 在容器工作负载中成功未来的关键。
CRI-O 维护人员很乐意听取有关新功能的任何反馈或建议!感谢您阅读这篇博客文章,请随时通过 Kubernetes Slack 频道 #crio 联系维护人员或在 GitHub 存储库中创建一个问题。