本文发布已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不再正确。

使用 seccomp 通知器查找可疑系统调用

调试生产环境中的软件是我们在容器化环境中必须面对的最大挑战之一。了解可用的安全选项的影响,尤其是在配置我们的部署时,是增强 Kubernetes 中默认安全性的关键方面之一。我们已经掌握了所有的日志、跟踪和指标数据,但我们如何将它们提供的信息组装成人类可读且可操作的内容?

Seccomp 是保护基于 Linux 的 Kubernetes 应用程序免受恶意操作的标准机制之一,它通过干扰其系统调用来实现。这允许我们将应用程序限制为一组定义的可操作项,例如修改文件或响应 HTTP 请求。将知道哪些系统调用集是修改本地文件等所需的,与实际源代码联系起来同样是非平凡的。Kubernetes 的 Seccomp 配置文件必须用 JSON 编写,可以理解为具有超能力的特定于架构的允许列表,例如

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 38,
  "defaultErrno": "ENOSYS",
  "syscalls": [
    {
      "names": ["chmod", "chown", "open", "write"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

上面的配置文件默认会出错,指定 defaultActionSCMP_ACT_ERRNO。这意味着我们必须通过 SCMP_ACT_ALLOW 允许一组系统调用,否则应用程序将无法执行任何操作。好吧,很酷,为了允许文件操作,我们所要做的就是添加一堆特定于文件的系统调用,例如 openwrite,并且可能还可以通过 chmodchown 更改权限,对吗?基本上是这样,但这种方法的简单性存在问题

Seccomp 配置文件需要包含启动应用程序所需的最小系统调用集。这也包括来自较低级别 开放容器倡议 (OCI) 容器运行时的一些系统调用,例如 runccrun。除此之外,我们只能保证特定版本的运行时和应用程序所需的系统调用,因为代码部分可能会在版本之间发生变化。这同样适用于应用程序的终止以及我们部署的目标架构。诸如在容器内执行命令之类的功能也需要另一组系统调用。更不用说,系统调用有多个版本,它们执行略有不同的操作,并且 seccomp 配置文件能够修改它们的参数。开发人员也并非总是清楚地知道他们自己编写的代码部分使用了哪些系统调用,因为他们依赖于编程语言的抽象或框架。

那么,我们如何知道哪些系统调用是必需的呢?谁应该在其开发生命周期中创建和维护这些配置文件?

好吧,记录和分发 seccomp 配置文件是 安全配置文件运算符 的问题域之一,它已经解决了这个问题。该运算符能够将 seccompSELinux 甚至 AppArmor 配置文件记录到自定义资源定义 (CRD) 中,将它们协调到每个节点,并使它们可供使用。

创建安全配置文件最大的挑战是捕获所有执行系统调用的代码路径。我们可以通过在运行端到端测试套件时对应用程序进行 100% 的逻辑覆盖来实现这一点。您会发现先前声明的问题:即使不考虑应用程序开发和部署期间的所有活动部件,它也过于理想化,永远无法实现。

在 seccomp 配置文件的允许列表中遗漏系统调用可能会对应用程序产生巨大的负面影响。不仅我们会遇到崩溃,这些崩溃很容易被检测到。它也可能稍微改变逻辑路径、改变业务逻辑、使应用程序的某些部分无法使用、降低性能甚至暴露安全漏洞。我们根本无法看到它的全部影响,尤其因为通过 SCMP_ACT_ERRNO 阻止的系统调用不会在系统上提供任何额外的 审计日志。

这是否意味着我们迷失了方向?梦想一个每个人都使用默认 seccomp 配置文件的 Kubernetes 是否不切实际?我们是否应该停止在 Kubernetes 中争取最大的安全性,并接受它不是默认安全的设计?

当然不是。技术会随着时间的推移而发展,并且在 Kubernetes 幕后有许多人在努力间接提供解决此类问题的功能。其中提到的一个功能是seccomp 通知器,可用于在 Kubernetes 中查找可疑的系统调用。

seccomp 通知功能包括 Linux 5.9 中引入的一组更改。它使内核能够将 seccomp 相关事件传达给用户空间。这允许应用程序基于系统调用采取行动,并为各种可能的用例打开了大门。我们不仅需要正确的内核版本,而且至少需要 runc v1.1.0(或 crun v0.19)才能使通知器正常工作。Kubernetes 容器运行时 CRI-Ov1.26.0 中获得了对 seccomp 通知器的支持。这项新功能允许我们识别应用程序中可能存在的恶意系统调用,从而可以验证配置文件的一致性和完整性。让我们尝试一下。

首先,我们需要运行 CRI-O 的最新 main 版本,因为撰写本文时 v1.26.0 尚未发布。您可以通过从源代码编译它,或通过get-script使用预构建的二进制包来实现。CRI-O 的 seccomp 通知器功能由一个注解保护,必须显式允许,例如使用这样的配置插入

> cat /etc/crio/crio.conf.d/02-runtimes.conf
[crio.runtime]
default_runtime = "runc"

[crio.runtime.runtimes.runc]
allowed_annotations = [ "io.kubernetes.cri-o.seccompNotifierAction" ]

如果 CRI-O 已启动并正在运行,则它也应指示 seccomp 通知器可用

> sudo ./bin/crio --enable-metrics
INFO[…] Starting seccomp notifier watcher
INFO[…] Serving metrics on :9090 via HTTP

我们还启用了指标,因为它们提供了有关通知器的其他遥测数据。现在我们需要一个正在运行的 Kubernetes 集群进行演示。对于此演示,我们主要坚持使用 hack/local-up-cluster.sh 方法在本地生成一个单节点 Kubernetes 集群。

如果一切都启动并正在运行,那么我们将必须定义一个 seccomp 配置文件以进行测试。但我们不必创建自己的,我们可以使用每个容器运行时都附带的 RuntimeDefault 配置文件。例如,CRI-O 的 RuntimeDefault 配置文件可以在 containers/common 库中找到。

现在我们需要一个测试容器,它可以是一个简单的 nginx pod,如下所示

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  annotations:
    io.kubernetes.cri-o.seccompNotifierAction: "stop"
spec:
  restartPolicy: Never
  containers:
    - name: nginx
      image: nginx:1.23.2
      securityContext:
        seccompProfile:
          type: RuntimeDefault

请注意注解 io.kubernetes.cri-o.seccompNotifierAction,它为此工作负载启用 seccomp 通知器。注解的值可以是 stop 以停止工作负载,也可以是任何其他值,除了记录和抛出指标外不执行其他操作。由于终止,我们还使用 restartPolicy: Never 以便在失败时不会自动重新创建容器。

让我们运行 pod 并检查它是否有效

> kubectl apply -f nginx.yaml
> kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP          NODE        NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          3m39s   10.85.0.3   127.0.0.1   <none>           <none>

我们还可以测试 Web 服务器本身是否按预期工作

> curl 10.85.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

虽然一切都已启动并正在运行,但 CRI-O 也表明它已启动 seccomp 通知器

…
INFO[…] Injecting seccomp notifier into seccomp profile of container 662a3bb0fdc7dd1bf5a88a8aa8ef9eba6296b593146d988b4a9b85822422febb
…

如果我们现在在容器内运行一个被禁止的系统调用,那么我们可以预期该工作负载将被终止。让我们通过在容器命名空间中运行 chroot 来尝试一下

> kubectl exec -it nginx -- bash
root@nginx:/# chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
root@nginx:/# command terminated with exit code 137

exec 会话已终止,因此看起来该容器不再运行

> kubectl get pods
NAME    READY   STATUS           RESTARTS   AGE
nginx   0/1     seccomp killed   0          96s

好的,该容器被 seccomp 终止了,我们是否获得了有关发生了什么事情的更多信息?

> kubectl describe pod nginx
Name:             nginx
Containers:
  nginx:
    State:          Terminated
      Reason:       seccomp killed
      Message:      Used forbidden syscalls: chroot (1x)
      Exit Code:    137
      Started:      Mon, 14 Nov 2022 12:19:46 +0100
      Finished:     Mon, 14 Nov 2022 12:20:26 +0100

CRI-O 的 seccomp 通知器功能正确设置了终止原因和消息,包括已使用多少次(1x)被禁止的系统调用。多少次?是的,通知器在上次看到的系统调用之后最多给应用程序 5 秒的时间,直到它开始终止。这意味着可以在一次测试中捕获多个被禁止的系统调用,而无需进行耗时的试错。

> kubectl exec -it nginx -- chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
command terminated with exit code 125
> kubectl exec -it nginx -- chroot /tmp
chroot: cannot change root directory to '/tmp': Function not implemented
command terminated with exit code 125
> kubectl exec -it nginx -- swapoff -a
command terminated with exit code 32
> kubectl exec -it nginx -- swapoff -a
command terminated with exit code 32
> kubectl describe pod nginx | grep Message
      Message:      Used forbidden syscalls: chroot (2x), swapoff (2x)

CRI-O 指标也将反映这一点

> curl -sf localhost:9090/metrics | grep seccomp_notifier
# HELP container_runtime_crio_containers_seccomp_notifier_count_total Amount of containers stopped because they used a forbidden syscalls by their name
# TYPE container_runtime_crio_containers_seccomp_notifier_count_total counter
container_runtime_crio_containers_seccomp_notifier_count_total{name="…",syscalls="chroot (1x)"} 1
container_runtime_crio_containers_seccomp_notifier_count_total{name="…",syscalls="chroot (2x), swapoff (2x)"} 1

它是如何详细工作的?CRI-O 使用选定的 seccomp 配置文件并注入操作 SCMP_ACT_NOTIFY 而不是 SCMP_ACT_ERRNOSCMP_ACT_KILLSCMP_ACT_KILL_PROCESSSCMP_ACT_KILL_THREAD。它还设置一个本地侦听器路径,较低级别的 OCI 运行时(runc 或 crun)将使用该路径来创建 seccomp 通知器套接字。如果已建立套接字和 CRI-O 之间的连接,则 CRI-O 将接收每个受 seccomp 干扰的系统调用的通知。CRI-O 存储系统调用,允许它们有一定的超时时间到达,如果选择了 seccompNotifierAction=stop,则终止容器。不幸的是,seccomp 通知器无法通知 defaultAction,这意味着需要有一个系统调用列表来测试自定义配置文件。CRI-O 也在日志中声明了该限制

INFO[…] The seccomp profile default action SCMP_ACT_ERRNO cannot be overridden to SCMP_ACT_NOTIFY,
        which means that syscalls using that default action can't be traced by the notifier

总而言之,CRI-O 中的 seccomp 通知器实现可用于验证应用程序在使用 RuntimeDefault 或任何其他自定义配置文件时是否表现正确。可以基于指标创建警报,以围绕该功能创建长期运行的测试场景。使 seccomp 易于理解和使用将提高采用率,并帮助我们朝着默认更安全的 Kubernetes 迈进!

感谢您阅读这篇博客文章。如果您想阅读更多关于 seccomp 通知器的信息,请查看以下资源