使用 seccomp 限制容器的系统调用

特性状态: Kubernetes v1.19 [stable]

Seccomp 代表安全计算模式,自 Linux 内核 2.6.12 版本以来一直是其特性。它可用于沙盒化进程的特权,限制它从用户空间向内核发出的调用。Kubernetes 允许你自动将加载到节点上的 seccomp 配置文件应用于你的 Pod 和容器。

识别工作负载所需的特权可能很困难。在本教程中,你将了解如何将 seccomp 配置文件加载到本地 Kubernetes 集群中,如何将它们应用于 Pod,以及如何开始制作只赋予容器进程必要特权的配置文件。

目标

  • 了解如何在节点上加载 seccomp 配置文件
  • 了解如何将 seccomp 配置文件应用于容器
  • 观察容器进程发出的系统调用审计
  • 观察指定缺失配置文件时的行为
  • 观察 seccomp 配置文件违规
  • 学习如何创建细粒度 seccomp 配置文件
  • 学习如何应用容器运行时默认 seccomp 配置文件

准备工作

要完成本教程中的所有步骤,你必须安装 kindkubectl

本教程中使用的命令假设你正在使用 Docker 作为你的容器运行时。(`kind` 创建的集群可能在内部使用不同的容器运行时)。你也可以使用 Podman,但在这种情况下,你必须遵循特定的说明才能成功完成任务。

本教程展示了一些仍处于 Beta 阶段(自 v1.25 起)的示例,以及其他仅使用通用 seccomp 功能的示例。你应该确保你的集群已针对你正在使用的版本正确配置

本教程还使用 `curl` 工具将示例下载到你的计算机。如果你愿意,可以调整步骤以使用其他工具。

下载示例 seccomp 配置文件

这些配置文件的内容将在稍后探讨,但现在请将它们下载到一个名为 `profiles/` 的目录中,以便可以将它们加载到集群中。

{
    "defaultAction": "SCMP_ACT_LOG"
}

{
    "defaultAction": "SCMP_ACT_ERRNO"
}

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "accept4",
                "epoll_wait",
                "pselect6",
                "futex",
                "madvise",
                "epoll_ctl",
                "getsockname",
                "setsockopt",
                "vfork",
                "mmap",
                "read",
                "write",
                "close",
                "arch_prctl",
                "sched_getaffinity",
                "munmap",
                "brk",
                "rt_sigaction",
                "rt_sigprocmask",
                "sigaltstack",
                "gettid",
                "clone",
                "bind",
                "socket",
                "openat",
                "readlinkat",
                "exit_group",
                "epoll_create1",
                "listen",
                "rt_sigreturn",
                "sched_yield",
                "clock_gettime",
                "connect",
                "dup2",
                "epoll_pwait",
                "execve",
                "exit",
                "fcntl",
                "getpid",
                "getuid",
                "ioctl",
                "mprotect",
                "nanosleep",
                "open",
                "poll",
                "recvfrom",
                "sendto",
                "set_tid_address",
                "setitimer",
                "writev",
                "fstatfs",
                "getdents64",
                "pipe2",
                "getrlimit"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

运行这些命令

mkdir ./profiles
curl -L -o profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
curl -L -o profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
curl -L -o profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
ls profiles

你应该在最后一步看到列出了三个配置文件

audit.json  fine-grained.json  violation.json

使用 kind 创建本地 Kubernetes 集群

为简单起见,可以使用 kind 创建一个加载了 seccomp 配置文件的单节点集群。Kind 在 Docker 中运行 Kubernetes,因此集群的每个节点都是一个容器。这允许将文件挂载到每个容器的文件系统中,类似于将文件加载到节点上。

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraMounts:
  - hostPath: "./profiles"
    containerPath: "/var/lib/kubelet/seccomp/profiles"

下载该示例 kind 配置,并将其保存到名为 `kind.yaml` 的文件中

curl -L -O https://k8s.io/examples/pods/security/seccomp/kind.yaml

你可以通过设置节点的容器镜像来指定特定的 Kubernetes 版本。有关此内容的更多详细信息,请参阅 kind 文档中关于配置的节点部分。本教程假设你正在使用 Kubernetes v1.34。

作为一项 Beta 功能,你可以将 Kubernetes 配置为默认使用容器运行时首选的配置文件,而不是回退到 `Unconfined`。如果你想尝试,请在继续之前参阅启用将 `RuntimeDefault` 作为所有工作负载的默认 seccomp 配置文件

一旦你有了 kind 配置,就使用该配置创建 kind 集群

kind create cluster --config=kind.yaml

新的 Kubernetes 集群准备就绪后,识别作为单节点集群运行的 Docker 容器

docker ps

你应该看到输出表明名为 `kind-control-plane` 的容器正在运行。输出类似于

CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                       NAMES
6a96207fed4b        kindest/node:v1.18.2   "/usr/local/bin/entr…"   27 seconds ago      Up 24 seconds       127.0.0.1:42223->6443/tcp   kind-control-plane

如果观察该容器的文件系统,你应该看到 `profiles/` 目录已成功加载到 kubelet 的默认 seccomp 路径中。使用 `docker exec` 在 Pod 中运行命令

# Change 6a96207fed4b to the container ID you saw from "docker ps"
docker exec -it 6a96207fed4b ls /var/lib/kubelet/seccomp/profiles
audit.json  fine-grained.json  violation.json

你已验证这些 seccomp 配置文件可供在 kind 中运行的 kubelet 使用。

创建一个使用容器运行时默认 seccomp 配置文件的 Pod

大多数容器运行时提供了一组合理的默认系统调用,这些系统调用是允许的或不允许的。你可以通过在 Pod 或容器的安全上下文中将 seccomp 类型设置为 `RuntimeDefault` 来采用这些默认值作为你的工作负载。

这是一个 Pod 的清单,它请求为其所有容器使用 `RuntimeDefault` seccomp 配置文件

apiVersion: v1
kind: Pod
metadata:
  name: default-pod
  labels:
    app: default-pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some more syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

创建该 Pod

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/default-pod.yaml
kubectl get pod default-pod

Pod 应该显示已成功启动

NAME        READY   STATUS    RESTARTS   AGE
default-pod 1/1     Running   0          20s

在进入下一节之前删除该 Pod

kubectl delete pod default-pod --wait --now

创建一个带有 seccomp 配置文件以进行系统调用审计的 Pod

首先,将 `audit.json` 配置文件(它将记录进程的所有系统调用)应用于新的 Pod。

这是该 Pod 的清单

apiVersion: v1
kind: Pod
metadata:
  name: audit-pod
  labels:
    app: audit-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

在集群中创建 Pod

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/audit-pod.yaml

此配置文件不限制任何系统调用,因此 Pod 应该成功启动。

kubectl get pod audit-pod
NAME        READY   STATUS    RESTARTS   AGE
audit-pod   1/1     Running   0          30s

为了能够与此容器公开的端点进行交互,请创建一个 NodePort 服务,该服务允许从 kind 控制平面容器内部访问该端点。

kubectl expose pod audit-pod --type NodePort --port 5678

检查服务在节点上分配了哪个端口。

kubectl get service audit-pod

输出类似于:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
audit-pod   NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

现在你可以使用 `curl` 从 kind 控制平面容器内部访问该端点,端口由该服务公开。使用 `docker exec` 在属于该控制平面容器的容器中运行 `curl` 命令

# Change 6a96207fed4b to the control plane container ID and 32373 to the port number you saw from "docker ps"
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

你可以看到进程正在运行,但它实际发出了哪些系统调用?因为此 Pod 在本地集群中运行,所以你应该能够在本地系统的 `/var/log/syslog` 中看到这些调用。打开一个新的终端窗口并 `tail` `http-echo` 的调用输出

# The log path on your computer might be different from "/var/log/syslog"
tail -f /var/log/syslog | grep 'http-echo'

你应该已经看到 `http-echo` 发出的一些系统调用日志,如果你再次在控制平面容器内部运行 `curl`,你将看到更多输出写入日志。

例如

Jul  6 15:37:40 my-machine kernel: [369128.669452] audit: type=1326 audit(1594067860.484:14536): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=51 compat=0 ip=0x46fe1f code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669453] audit: type=1326 audit(1594067860.484:14537): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=54 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669455] audit: type=1326 audit(1594067860.484:14538): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669456] audit: type=1326 audit(1594067860.484:14539): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=288 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669517] audit: type=1326 audit(1594067860.484:14540): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=0 compat=0 ip=0x46fd44 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669519] audit: type=1326 audit(1594067860.484:14541): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671648] audit: type=1326 audit(1594067920.488:14559): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671726] audit: type=1326 audit(1594067920.488:14560): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000

你可以通过查看每行上的 `syscall=` 条目来开始了解 `http-echo` 进程所需的系统调用。虽然这些不太可能包含它使用的所有系统调用,但它可以作为此容器的 seccomp 配置文件基础。

在进入下一节之前删除服务和 Pod

kubectl delete service audit-pod --wait
kubectl delete pod audit-pod --wait --now

创建一个带有 seccomp 配置文件导致违规的 Pod

为了演示,将一个不允许任何系统调用的配置文件应用于 Pod。

此演示的清单是

apiVersion: v1
kind: Pod
metadata:
  name: violation-pod
  labels:
    app: violation-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/violation.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

尝试在集群中创建 Pod

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/violation-pod.yaml

Pod 创建成功,但存在问题。如果你检查 Pod 的状态,你应该看到它未能启动。

kubectl get pod violation-pod
NAME            READY   STATUS             RESTARTS   AGE
violation-pod   0/1     CrashLoopBackOff   1          6s

如前面的示例所示,`http-echo` 进程需要相当多的系统调用。在这里,通过设置 `"defaultAction": "SCMP_ACT_ERRNO"`,seccomp 被指示在任何系统调用上出错。这非常安全,但消除了执行任何有意义操作的能力。你真正想要的是只赋予工作负载所需的特权。

在进入下一节之前删除该 Pod

kubectl delete pod violation-pod --wait --now

创建一个只允许必要系统调用的 seccomp 配置文件的 Pod

如果你查看 `fine-grained.json` 配置文件,你会注意到第一个示例中 syslog 中看到的一些系统调用,其中配置文件设置了 `"defaultAction": "SCMP_ACT_LOG"`。现在配置文件正在设置 `"defaultAction": "SCMP_ACT_ERRNO"`,但在 `"action": "SCMP_ACT_ALLOW"` 块中明确允许了一组系统调用。理想情况下,容器将成功运行,并且你将不会看到任何消息发送到 `syslog`。

此示例的清单是

apiVersion: v1
kind: Pod
metadata:
  name: fine-pod
  labels:
    app: fine-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/fine-grained.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

在集群中创建 Pod

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/fine-pod.yaml
kubectl get pod fine-pod

Pod 应该显示已成功启动

NAME        READY   STATUS    RESTARTS   AGE
fine-pod   1/1     Running   0          30s

打开一个新的终端窗口并使用 `tail` 监视日志条目,其中提到了来自 `http-echo` 的调用

# The log path on your computer might be different from "/var/log/syslog"
tail -f /var/log/syslog | grep 'http-echo'

接下来,使用 NodePort 服务公开 Pod

kubectl expose pod fine-pod --type NodePort --port 5678

检查服务在节点上分配了哪个端口

kubectl get service fine-pod

输出类似于:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
fine-pod    NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

使用 `curl` 从 kind 控制平面容器内部访问该端点

# Change 6a96207fed4b to the control plane container ID and 32373 to the port number you saw from "docker ps"
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

你应该在 `syslog` 中看不到任何输出。这是因为配置文件允许所有必要的系统调用,并指定如果调用了列表之外的系统调用,则应发生错误。从安全角度来看,这是一个理想的情况,但需要分析程序才能实现。如果有一种简单的方法可以在不需要太多努力的情况下更接近这种安全性,那就太好了。

在进入下一节之前删除服务和 Pod

kubectl delete service fine-pod --wait
kubectl delete pod fine-pod --wait --now

启用将 `RuntimeDefault` 作为所有工作负载的默认 seccomp 配置文件

特性状态: Kubernetes v1.27 [稳定]

要使用 seccomp 配置文件默认值,你必须为要使用它的每个节点启用 kubelet 的 `--seccomp-default` 命令行标志

如果启用,kubelet 将默认使用 `RuntimeDefault` seccomp 配置文件(由容器运行时定义),而不是使用 `Unconfined`(seccomp 已禁用)模式。默认配置文件旨在提供一组强大的安全默认值,同时保留工作负载的功能。默认配置文件可能因容器运行时及其发布版本而异,例如比较 CRI-O 和 containerd 的配置文件。

有些工作负载可能需要比其他工作负载更少的系统调用限制。这意味着即使使用 `RuntimeDefault` 配置文件,它们也可能在运行时失败。为了减轻此类失败,你可以

  • 明确将工作负载作为 `Unconfined` 运行。
  • 为节点禁用 `SeccompDefault` 功能。还要确保工作负载调度到禁用了该功能的节点上。
  • 为工作负载创建自定义 seccomp 配置文件。

如果你要在生产类集群中引入此功能,Kubernetes 项目建议你在部分节点上启用此功能门,然后在集群范围推广此更改之前测试工作负载执行。

你可以在相关的 Kubernetes 增强提案 (KEP) 中找到有关可能的升级和降级策略的更详细信息:默认启用 seccomp

Kubernetes 1.34 允许你配置当 Pod 规范未定义特定 seccomp 配置文件时应用的 seccomp 配置文件。但是,你仍然需要为要使用它的每个节点启用此默认设置。

如果你正在运行 Kubernetes 1.34 集群并希望启用此功能,请使用 `--seccomp-default` 命令行标志运行 kubelet,或通过 kubelet 配置文件启用它。要在 kind 中启用此功能门,请确保 `kind` 提供所需的最低 Kubernetes 版本,并在kind 配置中启用 `SeccompDefault` 功能。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            seccomp-default: "true"        
  - role: worker
    image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            seccomp-default: "true"        

如果集群准备就绪,那么运行 Pod

kubectl run --rm -it --restart=Never --image=alpine alpine -- sh

现在应该附加默认的 seccomp 配置文件。这可以通过使用 `docker exec` 在 kind worker 上为容器运行 `crictl inspect` 来验证

docker exec -it kind-worker bash -c \
    'crictl inspect $(crictl ps --name=alpine -q) | jq .info.runtimeSpec.linux.seccomp'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
  "syscalls": [
    {
      "names": ["..."]
    }
  ]
}

下一步

你可以了解更多关于 Linux seccomp 的信息

上次修改时间:2023 年 10 月 31 日太平洋时间上午 9:48:重构(content.en.docs.tutorials.security.SecComp):为 kindes/node 镜像添加 sha (1d7e34fcf2)