Containerd深入浅出-安全容器篇

2022-11-07 17:49:54 浏览数 (1)

Containerd 是一个高度模块化的高级运行时,所有模块均可插拔,模块均以 RPC service 形式注册并调用(gRPC 或者 TTRPC)。不同插件通过声明互相依赖,由 Containerd 核心实现统一加载,使用方可以自行实现插件以实现定制化的功能。当然这种设计虽然使得 Containerd 拥有强大的跨平台、可插拔的能力,同时也带来一些缺点,模块之间功能互调必须通过 RPC 调用。

文|lucy

编辑|zouyee

技术深度|简单

技术简介

K0s

K0s可以认为是一个 Kubernetes 发行版,是一个简易、稳定且经过认证的 Kubernetes 发行版,其由云计算服务供应商Mirantis推出,该版本强调简易醒与强健性,k0s针对各种工作负载的需求,均能够满足,无论是本地端部署,还是是大规模集群部署等。

Containerd

Containerd 是一个高度模块化的高级运行时,所有模块均可插拔,模块均以 RPC service 形式注册并调用(gRPC 或者 TTRPC)。不同插件通过声明互相依赖,由 Containerd 核心实现统一加载,使用方可以自行实现插件以实现定制化的功能。当然这种设计虽然使得 Containerd 拥有强大的跨平台、可插拔的能力,同时也带来一些缺点,模块之间功能互调必须通过 RPC 调用。

注:TTRPC 是一种基于 gRPC 的裁剪版通信协议。

从官方文档中可以看出,Containerd 被定义为 "它是为其宿主机系统提供完整容器生命周期管理的守护进程,从镜像传输与存储到容器执行和监控以及低级存储等"。

正如下图所示,Containerd 逐渐成为容器管理内核。

Containerd-shim

Runtime v2 为运行时实现者引入了一套shim API,以便与 Containerd 集成。shim API 只针对容器的执行生命周期管理。其是用于剥离 Containerd 守护进程与容器进程。目前已有 shim v1 和 shim v2 两个版本;它是Containerd 中的一个插件,其通过 shim 调用低级运行时命令来启动容器。

注:简单来说引入shim是允许低级运行时(如runc、youki等)在创建和运行容器之后退出, 并将shim作为容器的父进程, 而不是containerd作为父进程,是否还记得Containerd 抽象uds漏洞?

每一个 Containerd 容器都有一个相应的shim守护进程,这个守护进程会提供一个 API,Containerd 使用该 API 来管理容器基本的生命周期(启/停等),在容器中执行新的进程、调整 TTY 的大小以及与特定平台相关的其他操作。shim 还有一个作用是向 Containerd 报告容器的退出状态,在容器退出状态被 Containerd 收集之前,shim 会一直存在。这一点和僵尸进程很像,僵尸进程在被父进程回收之前会一直存在,只不过僵尸进程不会占用资源,而 shim 会占用资源。

创建集群

Mirantis引入了k0sctl配套的二进制文件,它只需要对一些Linux服务器进行ssh访问,以便在这些服务器上自动安装集群。K0s是作为单个二进制文件进行分发的,除了内核之外,它不依赖于主机操作系统,不需要特定的主机操作系统发行版,也不需要额外安装软件包。

代码语言:javascript复制
$ k0sctl init > k0sctl.yaml

该配置文件包含以下内容:

代码语言:javascript复制
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
  name: k0s-cluster
spec:
  hosts:
  - ssh:
      address: 10.0.0.1
      user: root
      port: 22
      keyPath: /Users/luc/.ssh/id_rsa
    role: server
  - ssh:
      address: 10.0.0.2
      user: root
      port: 22
      keyPath: /Users/luc/.ssh/id_rsa
    role: worker
  k0s:
    version: 0.10.0

可以修改上述文件,为K8s服务组件(kube-apiserver、kube-controller、kube-proxy)、网络插件(默认为Calico)和其他组件添加额外的配置选项。在目前的例子中,只是保留了默认的配置,但改变了IP地址以匹配预先提供的主机。

  • master node: 163.172.190.5
  • worker node: 163.172.190.5

用简单的命令启动集群创建(k0sctl.yaml是默认使用的配置文件)。

代码语言:javascript复制
$ k0sctl apply

生成的kubeconfig文件并配置本地kubectl。

代码语言:javascript复制
$ k0sctl kubeconfig -c k0sctl.yaml > kubeconfig
$ export KUBECONFIG=$PWD/kubeconfig

检查集群的节点,可以发现只列出了一个节点。master节点是专门用来运行管理控制平面的,因此不允许调度Pod。

代码语言:javascript复制
$ kubectl get no
NAME     STATUS   ROLES    AGE   VERSION
worker   Ready    <none>   5m  v1.20.2-k0s1

注意:创建k0s的主要原因之一是控制平面隔离。

kubelet是运行在集群每个节点上的agent,其需要与容器运行时进行通信,以便管理容器。为此,K0s默认提供containerd运行时,当然可以配置其他的运行时,如:

  • Docker
  • CRI-O

注意:由于Docker与CRI不兼容,所以在kubelet内部实现了dockershim服务,以便它能与Docker通信。dockershim在Kubernetes 1.20中声明废弃,并已经在Kubernetes 1.24版本着手删除工作,下图为满足CRI的运行时。

Containerd 认为是一个高级的容器运行时,它可以与下面列出的低级运行时进行交互。

runc是Open Container Initiative(OCI)指定的容器运行时的实现参照。其是诸多Kubernetes发行版默认安装和使用的容器运行时,当然,安装和使用其他低级容器运行时也非常方便。为了提高工作负载的安全性,可能需要其他的安全容器。

注意:下图实际docker(即moby)默认使用containerd容器运行时。

容器运行时

默认运行时

标准化的Kubernetes集群上运行一个简单的pod时,低级运行时容器默认是runc。创建的pod配置如下。

代码语言:javascript复制
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: www-runc
spec:
  containers:
  - image: nginx:1.18
    name: www
    ports:
    - containerPort: 80
EOF

通过查看工作节点上运行的进程,可以发现有多个containerd-shim-runc-v2的进程:一个用于新创建的pod,一个用于为启动的每个pod。目前有7个pod在运行。

代码语言:javascript复制
$ kubectl get po -A | awk '{print $1" "$2}'
default www-runc
kube-system calico-kube-controllers-5f6546844f-tw8t6
kube-system calico-node-4p98g
kube-system coredns-5c98d7d4d8-9z425
kube-system konnectivity-agent-m4gst
kube-system kube-proxy-jq6qd
kube-system metrics-server-6fbcd86f7b-m8cnd

逻辑关系可以简单表述如下。

kubelet → containerd → containerd-shim-runc-v2 → runc

GVisor

Google gVisor 是 Google 计算平台 (GPC) App Engine、Cloud Functions 和 CloudML 的sandbox基础。谷歌意识到在公共云基础设施中运行不受信任的应用程序的风险以及使用虚拟机沙箱应用程序的效率低下,由此开发了一个用户空间内核用来处理不受信任的应用程序。gVisor通过拦截应用程序发起的针对主机内核的系统调用,并在用户空间中通过Sentry处理这些系统调用。即使容器的恶意代码对内核进行破坏也是容器的内核,而非宿主机的内核。

简而言之,gVisor内核用Go语言编写,在用户空间运行,这个内核实现了Linux系统调用接口的很大一部分,并拦截应用程序的系统调用,从而提供了相应的保护,避免了主机内核的漏洞。gVisor组件包括一个runsc的Open Container Initiative(OCI)运行时。

在下文中,我们将配置containerd来通过runsc运行容器。

首先,使用下面的命令(详见k0s官方文档)来安装所有gVisor软件包。

代码语言:javascript复制
set -e
  URL=https://storage.googleapis.com/gvisor/releases/release/latest
  wget ${URL}/runsc ${URL}/runsc.sha512 
    ${URL}/gvisor-containerd-shim ${URL}/gvisor-containerd-shim.sha512 
    ${URL}/containerd-shim-runsc-v1 ${URL}/containerd-shim-runsc-v1.sha512
  sha512sum -c runsc.sha512 
    -c gvisor-containerd-shim.sha512 
    -c containerd-shim-runsc-v1.sha512
  rm -f *.sha512
  chmod a rx runsc gvisor-containerd-shim containerd-shim-runsc-v1
  sudo mv runsc gvisor-containerd-shim containerd-shim-runsc-v1 /usr/local/bin

然后,在containerd配置文件中添加一些配置,以便它可以使用gVisor作为其他的运行时。

代码语言:javascript复制
$ cat<<EOF | sudo tee /etc/k0s/containerd.toml
disabled_plugins = ["restart"]
[plugins.linux]
  shim_debug = true
[plugins.cri.containerd.runtimes.runsc]
  runtime_type = "io.containerd.runsc.v1"
EOF

接下来,重新加载其配置。

代码语言:javascript复制
$ kill -s SIGHUP CONTAINER_PID

注意:如果是大规模集群,建议使用批量工具,如ansible,或者使用高级节点管理工具等

紧接着,定义一个与gVisor的runsc运行时关联的RuntimeClass。

代码语言:javascript复制
$ cat<<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1beta1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
EOF

然后使用这个新的RuntimeClass运行Pod。

代码语言:javascript复制
$ cat<<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: untrusted
  name: www-gvisor
spec:
  runtimeClassName: gvisor
  containers:
  - image: nginx:1.18
    name: www
    ports:
    - containerPort: 80
EOF

列出带有app=untrusted标签的Pod,可以看到pod www-gvisor运行正常。

代码语言:javascript复制
$ kubectl get po -l app=untrusted
NAME         READY   STATUS    RESTARTS   AGE
www-gvisor   1/1     Running   0          9s

它使用gVisor/runsc,增加了一个额外的保护,因为系统调用首先由用户区内核处理。进程的调用链如下。

kubelet → containerd → containerd-shim-runsc-v1 → runsc

Kata Containers

kata container是为了解决容器安全的问题而诞生的,传统的容器是基于namespace和cgroup进行隔离,在带来轻量简洁的同时,也带来了一些安全的隐患。容器虽然提供一个与系统中的其它进程资源相隔离的执行环境,但是与宿主机系统是共享内核的,一旦容器里的应用逃逸到内核,后果不堪设想,尤其是在多租户的场景下。Kata就是在这样的背景下应运而生,kata很好的权衡了传统虚拟机的隔离性、安全性与容器的简洁、轻量。这一点和firecracker类似,都是轻量的虚拟机。但是他们的本质的区别在于:kata虽然是基于虚机,但是其表现的却跟容器是一样的,可以像使用容器一样使用kata;而firecracker虽然具备容器的轻量、极简性,但是其依然是虚机,一种比QEMU更轻量的VMM。

之前的版本使用了几个shim进程(containerd-shim、kata-shim、kata-runtime、kata-proxy),而目前的版本它只使用(container-shim-kata-v2)。

首先需要确保启用了嵌套虚拟化,因为工作节点上将为每个容器创建一个新的虚拟机,通过以下命令确认。

代码语言:javascript复制
$ cat /sys/module/kvm_amd/parameters/nested
1

安装kata容器包。

代码语言:javascript复制
$ bash -c "$(curl -fsSL https://raw.githubusercontent.com/kata-containers/tests/master/cmd/kata-manager/kata-manager.sh) install-packages"

接下来,修改containerd配置文件,使其使用kata作为另外的运行时(在前面步骤中添加的runc和gVisor/runsc之上)。

代码语言:javascript复制
$ cat<<EOF | sudo tee -a /etc/k0s/containerd.toml
[plugins.cri.containerd.runtimes.kata]
  runtime_type = "io.containerd.kata.v2"
EOF

接下来,重新加载其配置。

代码语言:javascript复制
$ kill -s SIGHUP CONTAINER_PID

注意:如果是大规模集群,建议使用批量工具,如ansible,或者使用高级节点管理工具等

接下来,创建 一个"kata "运行时的RuntimeClass。

代码语言:javascript复制
$ cat<<EOF | kubectl apply -f -
kind: RuntimeClass
apiVersion: node.k8s.io/v1beta1
metadata:
    name: kata
handler: kata
EOF

然后使用这个新的RuntimeClass运行Pod。

代码语言:javascript复制
$ cat<<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: untrusted
  name: www-kata
spec:
  runtimeClassName: kata
  containers:
  - image: nginx:1.18
    name: www
    ports:
    - containerPort: 80
EOF

列出带有app=untrusted标签的Pod,可以看到pod www-kata运行正常。

代码语言:javascript复制
$ kubectl get po -l app=untrusted
NAME         READY   STATUS    RESTARTS   AGE
www-gvisor   1/1     Running   0          8m1s
www-kata     1/1     Running   0          3h25m

它运行在一个专用的虚拟机(通常定义为microVM)中,该虚拟机只为该容器创建,还可以看到一个qemu进程正在运行。

代码语言:javascript复制
root@worker:~# ps -ef | grep qemu | awk '{print $11}'
/usr/bin/qemu-vanilla-system-x86_64

以下为相关调用链。

kubelet → containerd → containerd-shim-kata-v2 → kata

由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。

参考文献

1.https://betterprogramming.pub/kata-container-and-gvisor-with-k0s-82efbbcc240b

0 人点赞