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

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

Seccomp 代表安全计算(Secure Computing)模式,自 2.6.12 版本以来,一直是 Linux 内核的一个特性。 它可以用来沙箱化进程的权限,限制进程从用户态到内核态的调用。 Kubernetes 能使你自动将加载到节点上的 seccomp 配置文件应用到你的 Pod 和容器。

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

教程目标

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

准备开始

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

本教程中使用的命令假设你使用 Docker 作为容器运行时。 (kind 创建的集群可以在内部使用不同的容器运行时)。 你也可以使用 Podman,但如果使用了 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"
            ],
            "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.29。

作为 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 中运行命令:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的容器 ID
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:0.2.3
    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:0.2.3
    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 类型的 Service, 允许从 kind 控制平面容器内部访问端点。

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

检查 Service 在节点上分配的端口。

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 命令:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID 和端口号 32373
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

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

# 在你的计算机上,日志路径可能不是 "/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 配置文件的基础。

在转到下一节之前删除该 Service 和 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:0.2.3
    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 进程需要相当多的系统调用。 这里 seccomp 已通过设置 "defaultAction": "SCMP_ACT_ERRNO" 被指示为在发生任何系统调用时报错。 这是非常安全的,但消除了做任何有意义的事情的能力。 你真正想要的是只给工作负载它们所需要的权限。

在进入下一节之前删除该 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:0.2.3
    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 的调用的日志条目:

# 你计算机上的日志路径可能与 “/var/log/syslog” 不同
tail -f /var/log/syslog | grep 'http-echo'

接着,使用 NodePort Service 公开 Pod:

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

检查节点上的 Service 分配了什么端口:

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 控制平面容器内部访问端点:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID 和端口号 32373
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

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

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

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

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

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

要采用为 Seccomp(安全计算模式)设置默认配置文件这一行为,你必须使用在想要启用此行为的每个节点上启用 --seccomp-default 命令行标志来运行 kubelet。

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

与其他工作负载相比,某些工作负载可能需要更少的系统调用限制。 这意味着即使使用 RuntimeDefault 配置文件,它们也可能在运行时失败。 要应对此类故障,你可以:

  • 显式地以 Unconfined 模式运行工作负载。
  • 禁用节点的 SeccompDefault 特性。同时,确保工作负载被调度到禁用该特性的节点上。
  • 为工作负载创建自定义 seccomp 配置文件。

如果你将此特性引入到类似的生产集群中, Kubernetes 项目建议你在部分节点上启用此特性门控, 然后在整个集群范围内推出更改之前,测试工作负载执行情况。

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

Kubernetes 1.29 允许你配置 Seccomp 配置文件, 当 Pod 的规约未定义特定的 Seccomp 配置文件时应用该配置文件。 但是,你仍然需要为合适的节点启用这种设置默认配置的能力。

如果你正在运行 Kubernetes 1.29 集群并希望启用该特性,请使用 --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 上的容器运行 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 的更多信息:

最后修改 November 12, 2023 at 4:40 PM PST: [zh-cn] sync ns-level-pss.md seccomp.md (a8f1c1a367)