自定义容器安全:在Kubernete中使用seccomp notify

本文主要介绍如何在Kubernetes中通过seccomp notify机制来自定义容器的安全机制。

seccomp是什么

seccomp是Linux内核中用于限制应用使用 系统调用(System Calls) 的机制,类比于iptables是网络的防火墙,seccomp是系统调用的防火墙。seccomp包括STRICT模式、BPF模式(cBPF,非eBFP)。

  • STRICT:严格限制系统调用,只允许使用readwirte_exitsigreturn四个系统调用,对于其他的系统调用,会发送SIGKILL型号,使用场景有限。
  • BPF:当系统调用发生时,seccomp-bpf调用指定的bpf过滤程序,bpf程序通过返回SCMP_ACT_ALLOW,SCMP_ACT_KILL,SCMP_ACT_ERRNO等值,来决定系统调用如何处理。一般是允许或拒绝,也可以向调用者假装返回调用成功的code,但实际上未进行系统调用。
    seccomp-bpf需要静态加载一个bpf程序,一般不需要自己重头编写,而是使用libseccomp进行编写。通过libseccomp,我们只需要编写如下所示的json文件,就能实现seccomp-bpf对系统调用的过滤。
    {
        "defaultAction": "SCMP_ACT_ERRNO",  // 默认拒绝,白名单形式
        "syscalls": [
            {
                "names": [                  // 允许的系统调用
                    "accept",
                    "chown",
                    "kill",
                    "mmap",
                    ...
                ],
                "action": "SCMP_ACT_ALLOW",
                "args": [],
                "comment": "",
                "includes": {},
                "excludes": {}
            }
        ]
    }

seccomp用于容器

通过seccomp可以限制容器内进程的系统调用,防止恶意攻击。
docker可以通过--security-opt seccomp对seccomp进行配置,不配置的情况下使用docker的默认的seccomp配置

  • 不对容器开启seccomp
    $ docker run --security-opt seccomp=unconfined xxxx
  • 指定自定义的seccomp策略
    $ docker run --security-opt seccomp=/path/to/seccomp/config.json
    在只使用containerd的环境中,其配置方式与docker是一致的,containerd的default seccomp 也基本是与docker是一致的。

kubernetes中使用seccomp

在1.19之前的K8S版本中,可以通过添加seccomp.security.alpha.kubernetes.io/pod的Annotation来指定容器的seccomp配置:

apiVersion: v1
kind: Pod
metadata:
  name: some-pod
  labels:
    app: some-pod
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: localhost/profiles/some-profile.json
spec:

在1.19以后通过pod.spec.securityContext.seccompProfile来指定:

apiVersion: v1
kind: Pod
metadata:
  name: some-pod
  labels:
    app: some-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/some-profile.json
  containers:

其中type可以设置为:

  • Localhost:使用指定的seccomp的配置文件。
  • RuntimeDefault:使用runtime默认的seccomp配置,例如上面的docker、containerd。
  • Unconfined:关闭seccomp。
    另外,可以通过开启kubelet的SeccompDefault 特性,来对容器默认配置runtime default seccomp策略。

seccomp notify

虽然有了seccomp-bpf,但仍有诸多限制。seccomp-bpf整体上仍然是静态加载规则,bpf程序不能解引用指针(dereference pointer),去获取指针指向的内容。比如open("/tmp/file.log",O_WRONLY)的系统调用,在seccomp_dataargs中可能是[0x55fffff,0x0001],即open(0x55fffff,0x0001),而0x55fffff指向的/tmp/file.log并不能获得到。

// seccomp_data是bpf程序的输入
struct seccomp_data {  
    int nr;                         // system call number
    __u32 arch;                     // AUDIT_ARCH_* in <linux/audit.h>
    __u64 instruction_pointer;      // CPU instruction pointer
    __u64 args[6];                  // system call arguments
};

同时,cBPF也无法像eBPF那样通过BPF maps改变程序的行为。因此,无法实现例如:根据不同的pod、namespace执行不同的seccomp策略;或是阻塞容器的系统调用,直至Kubernetes裁决系统调用可以执行等一些场景。

seccomp notify可以解决这些问题。在seccomp notify机制下,seccomp会将系统调用的决定权转交给另一个用户态的进程,这个过程是通过seccomp返回对应的seccomp-bpf程序的文件描述符fd来实现的。以系统调用mount为例(kinvolk上的一个例子),具体的流程如下:

  • 程序调用mount()系统调用
  • seccomp执行bpf程序,收到返回值SCMP_ACT_MOTIFY
  • seccomp agent通过使用SECCOMP_IOCTL_NOTIF_RECV调用ioctl,获取seccomp_notify(结构如图所示)
  • seccomp agent从程序对应的/proc/pid/mem中读取mount()的参数,并执行mount操作(如果程序运行在容器里,需要进入对应的ns。例子中是执行mount操作,除此外可以根据需要进行审计记录等其他操作)
  • seccomp agent使用SECCOMP_IOCTL_NOTIF_SEND调用ioctl,返回seccomp_notify_resp(结构如图所示),告诉seccomp返回success。
  • seccomp返回0给程序

使用seccomp notify需要:

  • Runc >= 1.1.0
  • Libseccomp >= 2.5.0 (>= 2.5.2 recommended)
  • Linux kernel >= 5.9

kinvolk seccomp agent

随着runc实现对seccomp notify的支持,在K8S中使用seccomp notify也成为可能。简单来说,首先为runc运行的container配置bpf程序,用于返回notify;在收到seccomp的notify后,runc主进程调用seccomp agent(提前配置了socket地址),来进行加下来的处理逻辑。seccomp agent可以通过查询kube-apiserver来进一步实现复杂的处理逻辑。在runc中有个测试用的seccompagent ,实现了简单的对"chmod", "fchmod", "fchmodat", "mkdir"四个系统调用的,如果自己实现,则可以通过kinvolk/seccompagent 实现。

引用kinvolk社区的一张图,说明K8S+seccomp notify的整个流程。

1)配置pod的seccompProfile,指定配置文件为foo.json。文件中通过SCMP_ACT_NOTIFY 的action设置了哪些系统调用需要使用seccomp notify机制,同时通过listenerPath配置了seccomp agent的调用地址。除此外,还可以通过listenerMetadata 指定传递给seccomp agent的参数。
2)pod调度到节点,containerd根据设置的foo.json生成配置调用runc启动容器。
3)在pod调用mount后,runc收到seccomp返回的fd,将container process state与fd发送到seccomp agent中,由seccomp agent完成处理逻辑。

kinvolk/seccompagent代码库的结构:

  • pkg/agent:agent的实现,启动socket监听,接受OCI hook传递过来的fd。
  • pkg/handlers:一些基本的系统调用实现,比如mkdirmount
  • pkg/kuberesolver:用于实现基于Kubernetes Pod的自定义handler
  • pkg/nsenter:用于操作、进入其他namespace
  • pkg/readarg:用于获取系统调用的参数
  • pkg/registry:用于注册一组系统调用的handler

Demo

在kinvolk/seccompagent中有个Demo,下面以mkdir的调用演示功能。
为了简化demo,对官方的Demo部分代码做了精简:

$ cat demo.go
package main

import (
	"fmt"
	"github.com/kinvolk/seccompagent/pkg/agent"
	"github.com/kinvolk/seccompagent/pkg/handlers"
	"github.com/kinvolk/seccompagent/pkg/kuberesolver"
	"github.com/kinvolk/seccompagent/pkg/nsenter"
	"github.com/kinvolk/seccompagent/pkg/registry"
)

func main() {
	nsenter.Init()
	kubeResolverFunc := func(pod *kuberesolver.PodContext, metadata map[string]string) *registry.Registry {
		r := registry.New()
		r.SyscallHandler["mkdir"] = handlers.MkdirWithSuffix(fmt.Sprintf("-%s-%s", pod.Pod, pod.Namespace)) // 注册mkdir的系统调用handler
		return r
	}
	resolver, err := kuberesolver.KubeResolver(kubeResolverFunc)
	if err != nil {
		panic(err)
	}
	err = agent.StartAgent("/run/seccomp-agent.socket", resolver)
}

$ cat notify.json
{
   "architectures" : [
      "SCMP_ARCH_X86",
      "SCMP_ARCH_X32"
   ],
   "defaultAction" : "SCMP_ACT_ALLOW",
   "listenerPath": "/run/seccomp-agent.socket",
   "syscalls" : [
      {
         "action" : "SCMP_ACT_NOTIFY",
         "names" : [
            "mkdir"
         ]
      }
   ]
}

Dockerfile创建镜像,部署seccomp agent 与测试用的busybox,将notify.json拷贝到 /var/lib/kubelet/seccomp/notify.json

$ cat busybox.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox-deploy
  namespace: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: busybox-deploy
  template:
    metadata:
      labels:
        app: busybox-deploy
    spec:
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: notify.json #相对于/var/lib/kubelet/seccomp的文件路径
      tolerations:
        - key: "node-role.kubernetes.io/master"
          operator: Exists
      containers:
        - image: busybox
          command: ["sleep","3600"]
          imagePullPolicy: IfNotPresent
          name: busybox
$ ls /var/lib/kubelet/seccomp/notify.json
/var/lib/kubelet/seccomp/notify.json
$ kubectl get po -n seccomp-agent -o wide
NAME                  READY   STATUS    RESTARTS   AGE     IP            NODE       NOMINATED NODE   READINESS GATES
seccomp-agent-7z2bl   1/1     Running   0          2m31s   10.222.0.20   maao-dev   <none>           <none>
$ kubectl get po -n test -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
busybox-deploy-697468b5dd-dxr7q   1/1     Running   0          2m    10.222.0.21   maao-dev   <none>           <none>

在busybox里调用mkdir,会由seccomp agent创建带有相应后缀的文件夹:

$ kubectl exec -it busybox-deploy-697468b5dd-dxr7q -n test sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # mkdir a
/ # ls
a-busybox-deploy-697468b5dd-dxr7q-test  etc                                     root                                    usr
bin                                     home                                    sys                                     var
dev                                     proc                                    tmp

参考

Bringing Seccomp Notify to Runc and Kubernetes(主要介绍Seccomp Notify在Kubernetes里的实践,里面有链接很多相关的博文)
Seccomp Notify – New Frontiers in Unprivileged Container Development(很全面的一篇文章,从seccomp介绍到seccomp notify)
Hardening Docker and Kubernetes with seccomp(主要介绍Kubernetes中原生的seccomp使用)
Seccomp Notify on Kubernetes FOSDEM 2021的一个分享