前言
随着容器及Kubernetes的广泛使用,越来越多的容器安全与隔离问题被暴露出来,如:容器逃逸、水平攻击、DDos攻击等严重威胁了办公和生产环境的安全与稳定,影响了业务的正常运行。安全容器技术孕育而生,产生了Kata、gVisor、unikernel等多种安全容器方案。本文旨在介绍各种安全容器方案,分析各方案特点,结合腾讯在容器安全领域的实践,帮助读者选择适合自身特性的容器运行时。同时引入Rust-VMM项目,介绍 Rust-VMM 技术和生态,演示如何使用Kubernetes调度和启用Rust-VMM安全容器运行时,展望以Rust语言实现的容器运行时的广阔前景。
容器安全与隔离
一个基于Kubernetes集群构建的基础设施中,内部存在不同层次的隔离,从容器到Pod再到节点最后到cluster,每一层隔离都有它的特点和特性,我们尤其关注Pod级别的隔离特性。
相比其他层次的安全隔离,Pod及容器级别的隔离对我们的挑战非常大。容器在运行时使用root运行进程,尽管使用namespace技术为容器空间内的pid、uts、fib等进行了隔离,但由于各个容器共享系统内核,容器与内核间缺乏隔离保护,容易引发容器逃逸等安全问题,典型容器逃逸攻击如:CVE-2018-14634、CVE-2016-5195、CVE-2019-5736 及 CVE-2019-14271等。
docker.vh.neargle.com:8888/?command_exec=python3 -c "import docker;client = docker.DockerClient(base_url='unix:///var/run/docker.sock');data = client.containers.run('alpine:latest', r'''sh -c \"echo 'ssh-rsa xxxxx root@620e839e9b02' >> /tmp/root/root/.ssh/authorized_keys\" ''', remove=True, volumes={'/': {'bind': '/tmp/root', 'mode': 'rw'}})"
上述脚本是一个简单的例子,这段代码会向docker.sock的端口发起请求,拉起一个alpine的容器,容器内进程会向所在主机注入一段SSH的公钥。在容器里的恶意用户或者攻击者,就可以获轻松得这个容器所在host主机的SSH的登录权限,从而能够非法查看同主机其他容器空间的信息,篡改关键文件或记录,甚至以主机为跳板攻击整个集群。
还有一个就是Noisy Neighbor,就是吵闹邻居问题。关于Noisy Neighbor,容器方面已经有了很多进展,比如对于CPU、memory、bandwidth甚至是buffer IO,基于Cgroup对这些资源已经有了一些隔离和限制,但是这些限制并不能完全解决Noisy Neighbor的问题,还是有一些吵闹的进程会影响到正常的业务进程的运行。
# kubectl run --rm -it bb --image=busybox sh
/ # f(){ f|f& };f # WARNING: Don't try this!
上面是一个简单的例子,启动一个busybox的容器,在里面执行一个嵌套循环的指令,会把这台主机上所有的file descriptor全部耗尽,造成这台主机上正常运行的业务进程工作不正常,这个是Noisy Neighbor的风险和问题。
对于上述问题,建议用户关注官方的漏洞报告,升级操作系统或Docker的版本,根据安全指引配置容器环境,同时可以参考以下措施增强容器集群的安全防护级别。
- 在物理层面上隔离,为不同的租户之间划分不同的Hardware Isolation域,让不同的租户使用不同的硬件空间,从物理上、网络上以及存储上彻底的隔离,这也是最直接最有效的方法。
- 利用一些Security Tools,包括经常用的SElinux或者Cgroup隔离,划分不同的资源访问和安全规则,但是这些安全规则需要编写大量的profile文件,实现起来难度颇大。
- 入侵检测机制,主机防御的一种手段。入侵检测的软件或者进程会监控这台主机上有风险的进程或者有风险的文件,对于这些文件的读写操作都会有安全方面的记录,会即时预警,即时响应。比如对于containerd-shim/busybox/docker-runc的关键进程,比如docker-runc对于bash、init或者对于fd的访问都可以做到即时的预警和标记。对于上面提到的容器逃离漏洞,通过入侵检测的机制,就可以有一个比较有效的防御。
- 定制Linux Kernel Patch,一些特殊的资源隔离或者安全漏洞,也可以为kernel打一些自身特有的patch来加以防范,但是这里的patch面临的问题是多种多样的,所以这里就不再展开细讲了。
容器运行时
上述安全实践方案和措施能够很大程度的减少对外提供服务时受攻击的范围,提高容器服务的安全能力。但我们仍然想要寻找一种简单的、有效的、统一的runtime解决方法,我们把眼光投入到CNCF runtime Landscape,可以看到有多种解决方案。
简单梳理一下这些解决方案,都是按照两大标准划分的。一个就是CRI的标准,偏向于kubelet或者Kubernetes这一侧的标准。还有一侧的标准就是OCI,就是偏向于容器底层基础实现。
可以看到OCI这边有很多种实现方案,简单梳理一下,划分了一个表格:
简单介绍一下,比如有runC的方案,就是基于原生namespace的容器方案,Docker使用的,containerd直接对接的runc的方案。gVisor是谷歌开源出来的一种基于用户态的内核进程的安全沙箱技术的方案。Kata和Qemu的基于虚拟化实现安全容器,这个现在也是比较热门,使用比较广泛的一种方案。Firecracker,由AWS开源出来的,Firecracker Containerd是对接OCI标准的组件,但现在还没有完全对接OCI标准,所以这里是有所欠缺的。最后是Nabla,runnC是Nabla对接OCI实现的一个组件,Nabla是IBM开源的一套安全容器的方案,但是它跟上述的一些方案有所区别,它是一个基于unikernel的方案,把业务应用和内核完全编译在了一起,直接运行在host上面,所以可以看到它跟其他的方案不一样的就是它不能直接使用容器的镜像,需要自己编译一个带着内核的特殊镜像,所以用起来不太符合容器的标准。
比较下来,典型的三种安全容器方案就是runC,kata-runtime,以及 gVisor 方案。
三种方案各有优缺点,从架构来说,runC的方案最简单,直接用原生的namespace的方案。Kata是基于hypervisor的方案,就是基于虚拟化的方案,所以它的每一个容器或者它的容器是运行在一个一个guest kernel上面,就是guest的虚拟机里,然后会有一个kata agent负责runtime跟底层真正跑的container的进行通信。gVisor是介于runc和kata runtime之间,它不像Kata有一个虚拟化的Guest Kernel的存在,它是把Guest Kernel变成了一个运行于用户态的一个内核的进程叫做Sentry,Gofer是它IO的组件,核心的sentry负责的就是拦截和劫持所有运行在它的沙箱里面容器的所有的Syscall,对系统调用进行过滤和保护。
最后汇总比较一下,runC的方案实现最简单,而且它的性能也是最好的。但是一大缺点就是安全隔离的特性不够安全,所以需要大量的外部Security Tools的保护。基于kata的虚拟化的沙箱方案,它只能运行在支持KVM虚拟化的环境上面,因为他的虚拟层是基于KVM实现的。Kata的逻辑的链路很长,调用的路径也非常长,会造成一些性能上的损耗,也会有一些问题。gVisor介于两者之间,但是gVisor现在的问题就是它的兼容性不够,300多个系统调用只能兼容里面的一部分,并且Sentry的作为核心会有一些性能瓶颈。
腾讯云在安全容器上融合和上述方案的优点,结合腾讯云在虚拟化,存储和网络方面的优势, 选择使用mVMd + QEMU + EKLET的方案,实现一个弹性的Kubernetes的服务,即EKS,大家可以访问一下腾讯云的官网,看一下EKS的介绍。
EKS是基于Hypervisor的虚拟化的解决方案,不同于Kata,EKS使用的containerd + mVMd组件更加轻量,调用路径更短。通过containrtd + mVMd来实现对于上层Kubernetes调用的CRI的解析,并且把它转化成真正对于底层一个一个Guest VM或者QEMU的控制指令,在guest VM里会启动相应的containers容器。
在整个的架构中,在Runtime方面最大的瓶颈是QEMU,因为QEMU有几十年的历史了,所以存在着它比较臃肿,反应慢,占用的资源多等等问题。所以让QEMU作为底层Runtime还不够快,不够安全。为了增强QEMU的性能和安全特性,我们很自然把眼光投向了Rust-Vmm。
Rust-Vmm 介绍
Rust-Vmm是一个开源工程,是一个可以自由定制的VMM(virtual machine monitor)虚拟机管理器,用户可以按照自己的方式订制它。它是基于Rust语言实现的VMM,有着Rust语言带来的优点和特性。
首先,Rust语言一个内存安全的语言,相比于用C或者C++会频繁遇到的各种内存的问题,比如内存的溢出、空指针、野指针、越界访问等等,更进一步会造成安全的问题、性能的问题,以及各种崩溃的问题。Rust语言很好地解决了这一点,从它的语法、编译规则等杜绝了内存级别访问的漏洞以及风险,所以用Rust写的Rust-Vmm天然的就是内存安全的。
第二,Rust-Vmm是不易被攻击的,Rust-VMM是从零开始的,它是从最小的硬件虚拟化出发的,最小的硬件虚拟化意味着它有着最小的攻击面,被攻击的面就非常少,所以它会很安全。
第三,Rust-Vmm能够很灵活的定制。Rust-VMM可以灵活定制它的每一个组件,所有的对于设备的模拟或者关键特性的处理都是封装成了一个一个的Rust-Vmm crates包,比如有VCPU,有linuxloader,vm-virtIO等等。其中crates是Rust语言中的包管理工具,可以理解JAVA或golang里面的package,它是以发行不同的包或者库的形式对外发布它的feature。
第四,Rust-Vmm有非常高的性能,基于Rust语言的without garbage collection特性,它是没有GC回收检查机制的,不像JAVA或者其他更高级的语言会有一个runtime,Rust-Vmm的性能上会更好,同时基于KVM实现的虚拟化方案也是性能的保证。
简单介绍一下Rust-Vmm的一个历史,它是由谷歌首先实现的,谷歌首先实现一个Rust based的轻量级的VMM,它叫做crosVM,大家也可以从链接里面看到,它是一个为chrome浏览器做的一个微内核。然后AWS,亚马逊基于谷歌开源出来的crosVM,实现了自己的基于rust的VMM叫Firecracker。两个项目的开发人员会发现做这两个项目的时候,会有很多重复的重叠的通用的代码,很自然的把可以开源的、通用的部分结合到一块,就有了Rust-Vmm的项目。
从这个全景图里面可以看到,Rust-Vmm应用在多个项目和产品中。从最开始的谷歌开源的crosVM里面会有Rust-Vmm,比如AWS的Firecracker、以及其它CSP云服务器厂商,比如QEMU里面会把一些耗时的或者内存访问的部分也用Rust-Vmm实现,还有开源的Cloud Hypervisor项目。
Cloud Hypervisor是Intel开源出来的一套框架,把Rust-Vmm的组件组合在一起,能够对外提供一个VMM的完整服务。用户可以配置使用已公开发布的crates包,或集成自己的开发的crates包,通过将各种crates包组合在一起,就可以生成一个定制的安全的高性能的VMM。
基于 Rust-Vmm 实现 Kubernetes Runtime
讲到这里,基于Rust-VMM的一个实现,组件、原料和所有的知识都已经具备了,接下来就是如何对接Kubernetes,基于Rust-VMM实现Kubernetes runtime,运行一个Kubernetes的Pod。
测试环境如图所示,用到了包括Kubernetes,containerd,Kata,Rust-Vmm,Cloud Hypervisor等开源项目,首先我们配置Kubernetes节点上的kubelet使用的runtime为containerd,在containerd上配置了Kata-runtime作为容器的运行时,再配置了Kata-runtime使用的vmm为基于Rust-Vmm crates构建的Cloud Hypervisor,同时为了提升性能,这里也是用了virtos-fs技术来承载物理机上的容器镜像到子机内容器的imagefs的共享,所以在物理机上需要额外开启virtiofsd的进程,在guest os的内核上也是使用最新的包含virtio-fs支持的内核版本。
在Kubernetes上创建一个Pod后,调度器调度这个Pod到我们这个环境上的一个worker节点,节点上的kubelet进程根据我们的配置调用containerd,containerd的runtime插件kata来创建Cloud Hypervisor的vmm(virtual machine monitor),在主机内预置的Guest OS操作系统启动后,kata-agent也随之启动,成功建立母机与子机的通道,后续cri协议中的createcontainer,startcontainer以及停止容器等接口也由这个通道传递到子机内的kata-agent,由其来调用子机内真正的容器运行时,从而完成整个Pod的生命周期。
我们来看一下最终达到的效果,如图中第一条指令,我们执行kubectl get pod命令可以看到pod的状态为运行中。同样的,我们也可以通过crictl pods命令查询到这个Pod的状态。通过ps查询进程列表,可以看到包含这个pod id的kata shim进程,virtiofsd进程以及cloud-hypervisor这个vmm的进程。最后我们也可以通过kubectl exec -it交互式的进入pod的container内,通过uname返回此容器使用的内核版本为5.6,物理机的内核版本为5.4。
总结与展望
最后是对未来的展望,基于Rust-Vmm我们想要做得更多。比如希望扩展一下Rust-Vmm在网络以及存储方面的一些特性,尤其是在腾讯云的环境里面,通过结合CBS和VPC,提升Runtime的性能。
第二,会结合入侵检测、主机安全、安全工具等,把它们融合在一起,构建起一个多维的、多重防护的一套全维度安全的容器服务平台,Rust-VMM会实现里面的一部分。
第三,扩展Rust Runtime,尤其在边缘计算领域,在边缘端能够实现Kubernetes以下完全由Rust接管,由Rust语言全部重写,比如底层的Containerd、 shim等组件,变成一个Full Stack Rust的实现。
最后,Rust-VMM也是基于hypervisor来实现,所以基于hypervisor的一些特性,比如热迁移、备份、回滚这些机制,未来我们希望都可以在Rust-Vmm上进行实现。