Phil Karlton 有一句名言:“计算机科学界只有两个真正的难题——缓存失效和命名。”
对我们容器圈而言,我相信「命名」绝对配得上这句话。这毫无疑问是一件让老开发者沉默、让新人落泪的事情。仅就系统软件而言,我们当今比较通行地称为「Linux 容器技术」这个概念,它曾经用过的名字还有 Jail, Zone, Virtual Server, Sandbox 等。同样,在早期虚拟化的技术栈里也把一类虚拟机叫做容器,毕竟这个词本身就指代那些用来包容、封装和隔离的器物。它实在太过常见了,以至于以严谨著称的 Wikipedia,它的词条叫做「OS-Level Virtualization」(系统级虚拟化) ,从而回避了「什么是容器」这个问题。
在 2013 年,Docker 问世之后,容器这个概念伴随着不可变基础设施、云原生这一系列概念在随后的几年间以摧枯拉朽之势颠覆了基于“软件包+配置”这种细粒度组合的应用部署,用简单的声明式策略和不可变的容器就清爽地定义了软件栈。应用怎么部署,在这儿似乎有点离题了,我在这里想要强调的是:
“云原生语境下的容器,实质是「应用容器」——是以标准格式封装的,运行于标准操作系统环境(常常是 Linux ABI)上的应用打包——或运行这一应用打包的程序/技术。”
这个定义是我下的,但它并不是我的个人意志,是基于 OCI 规范这一共识写出来的。这个规范规定了容器之中应用被放到什么样的环境下、如何运行,比如说容器的根文件系统上哪个可执行文件会被执行,是用什么用户执行,需要什么样的 CPU,有什么样的内存资源、外置存储,还有什么样的共享需求等等。
所以说,标准格式的封装、标准的操作系统环境在一起以应用为中心就构成了应用容器的打包。
以这个共识为基础,就可以来说说安全容器了。当年,我和我的联合创始人赵鹏使用「虚拟化容器」这个名字来命名我们的技术的,不过为了博人眼球,我们用了「Secure as VM, Fast as Container」这样的 Slogan,于是,被容器安全性问题戳中心坎的人们立刻用「Secure Container」或者说「安全容器」来称呼这种东西了,一发而不可收。虽然在我们的内心里,这个技术是一层额外的隔离,它只是安全中的一环,但是呢,用户还是愿意用安全容器这个名字来称呼它。我们给安全容器下的定义就是:
安全容器是一种运行时技术,为容器应用提供一个完整的操作系统执行环境(常常是 Linux ABI),但将应用的执行与宿主机操作系统隔离开,避免应用直接访问主机资源,从而可以在容器主机之间或容器之间提供额外的保护。
这就是我们的安全容器。
二、间接层:安全容器的精髓
说安全容器的时候,就要提到「间接层」这个词。它出自于 Linus Torvalds 在 2015 年的 LinuxCon 上提出的:
“安全问题的唯一正解在于允许那些(导致安全问题的)Bug 发生,但通过额外的隔离层来阻挡住它们。”
为了安全,为什么要引入隔离层呢?其实 Linux 本身这样的规模是非常大的,无法从理论上来验证程序是没有 Bug 的,于是,一旦合适的 Bug 被利用,安全性风险就变成安全性问题了。安全性的框架和修补并不能确保安全,所以我们需要进行一些额外的隔离来减少漏洞以及因为这些漏洞造成的被彻底攻破的风险。
这就是安全容器的由来。
三、Kata Containers:云原生化的虚拟化
2017 年 12 月,我们在 KubeCon 上对外发布了 Kata Containers 的安全容器项目,这个项目有两个前身:由我们之前开始的 runV 以及 Intel 的 Clear Container 项目。这两个项目都是 2015 年 5 月开始开展的,实际上是早于 Linus 在 KubeCon 2015 说的那番话的。
它们的思路都很简单:
操作系统本身的容器机制没法解决安全性问题,需要一个隔离层;
虚拟机本身,VM,它是一个现成的隔离层,比如说像阿里云、AWS,它们都使用了虚拟化技术,所以对于全世界来说,大家已经普遍地相信,对于用户来说,只要能做到「secure of VM」,那这个安全性就可以满足公有云的需求了;
虚拟机中如果有个内核,就可以支持我们刚才所提到的 OCI 的定义,也就是说提供了 Linux ABI 的运行环境,在这个运行环境中跑一个 Linux 应用不太难实现。
现在的问题是虚机不太够快,阻碍了它在容器环境中的应用,如果能拥有「speed of container」的话,那我们就可能可以有一个用虚拟机来做隔离的安全容器技术了。这个也就是 Kata Containers 本身的一个思路,就是用虚拟机来做 Kubernetes 的 PodSandbox。在 Kata 里面被拿来做 VM 的先后有 qemu, firecracker, ACRN, cloud-hypervisor 等。
下图就是 Kata Containers 怎么去和 Kubernetes 集成的,这里的例子用的是 containerd,当然 CRI-O 也是一样的。
目前,Kata Containers 通常是在 Kubernetes 中使用。首先 Kubelet 通过 CRI 接口找到 containerd 或者 CRI-O,这个时候比如镜像这样的操作一般也是由 containerd 或者 CRI-O 来执行的。根据请求,它会把 runtime 部分的需求变成一个 OCI spec,并交给 OCI runtime 执行。比如说上图上半部分中的 kata-runtime,或者说下半部分精简过后的 containerd-shim-kata-v2。具体的过程是这样的:
当 containerd 拿到一个请求的时候,它会首先创建一个 shim-v2。这个 shim-v2 就是一个 PodSandbox 的代表,也就是那个VMM 的代表;
每一个 Pod 都会有一个 shim-v2 来为 containerd/CRI-O 来执行各种各样的操作。shim-v2 会为这个 Pod 启动一个虚拟机,在里面运行着一个 linux kernel,也就是图里面的 Guest kernel。如果这个里面用的是 qemu 的话,我们会通过一些配置和一些补丁,让它变得小一些。同时这个里面也没有额外的 Guest 操作系统,不会跑一个完整的像 CentOS, Ubuntu 这样的操作系统;
后我们会把这个容器的 spec 以及这个容器本身打包的存储,包括 rootfs 和文件系统,交给这个 PodSandbox。这个 PodSandbox 会在虚机中由 kata-agent 把容器启动起来;
依照 CRI 语义和 OCI 规范,在一个 Pod 里面是可以启动多个相关联的容器的。它们会被放到同一个虚拟机里面,并且可以根据需求共享某些 namespace;
除了这些之外,其它的一些外置的存储和卷也可以通过热插拔的方式来插到这个 PodSandbox 里面来;
对于网络来说,目前使用 tcfilter 就可以无缝地接入几乎所有的 Kubernetes 的 CNI 插件。而且我们还提供了一个 enlightened 的模式,这样的话会有一个特制的 CNI 插件来提高容器的网络能力。
可以看到,在我们的 PodSandbox 里面,实际上只有一个 Guest Kernel 跑着一些容器本身的打包和容器应用,并不包含一个完整的操作系统。就是说,这个过程,它用起来并不像是传统的虚拟机,对于容器来说,它只有容器的引擎,并且通过少用不必要的内存、共享能共享的内存来进一步地降低内存的开销。
与传统的虚拟机比起来,开销更小、启动更轻快,对于大部分的场景来说,它可以做到「secure as VM」、「fast as container」。同时,在安全性技术以外,相比传统的虚机,它有更多的弹性,更少了机器的那种物理操作的手感,比如说这里面说过的包括动态资源的插拔以及使用 virtio-fs 这样的技术等。它是一个专门为我们这种场景、为像 kata 这样的场景来做的一个把 host 的基本文件系统的内容(比如说容器的 rootfs )共享给虚拟机的这样一个技术。
通过其中一些之前为非易失存储、非易失内存来做的 DAX 的技术,能够在不同的 PodSandbox 之间,也就是不同的 Pod 之间、不同的容器之间,共享一些可以共享的只读的内存部分。这样可以在不同的 PodSandbox 之间去节省很多的内存。同时所有的 Pod 的管理都是通过 Kubernetes 从外部进行的容器管理,并且从外部来获取 metrics 和 debug 信息,并没有登陆虚拟机这样一种手感。所以它看起来是一种非常容器化的操作,虽然从底层来看,它还是一个虚拟机,但是实际上它是一个面向云原生的虚拟化。
四、gVisor:进程级虚拟化
gVisor,我们又把它叫做进程级的虚拟化,它是和 kata 不一样的另外一种方式。
在 2018 年的 5 月份,哥本哈根的 KubeCon 上,Google 开源了他们内部开发了 5 年的 gVisor 安全容器作为对 kata containers 的回应,表明了他们有一种不同的安全容器的解决方案。
如果说 Kata Containers 是通过对现有的隔离技术进行组合和改造来构建容器间的隔离层的话,那么 gVisor 的设计显然是更加简洁的。
如上图右侧所示,它是一个用 Go 语言重写的运行在用户态的操作系统内核,这个内核的名字叫做 sentry,它并不依赖于虚拟化和虚拟机技术,相反,它是借助一个它们内部叫做一个 Platform(平台)的能力,让宿主机的操作系统做一个操作,把应用所有的期望对操作系统的操作都转交给 sentry 来进行,sentry 做处理之后会把其中的一部分交给操作系统来帮它完成,大部分则由自己来完成。
gVisor 是一个纯粹的面向应用的隔离层,从一开始就不是一个完全等同于虚拟机的东西,它就是用来在 Linux 上面跑一个 Linux 程序的。作为一个隔离层,它的安全性依据在于:
gVisor 的开发者们首先要把攻击面变小,宿主机的操作系统将只为沙箱里的应用提供大约 20% 的系统调用;
Linux 大概有 300 多个 Syscall,实际上 sentry 最后向操作系统发起的调用只会集中在 60 多个 Syscall 上。这个是源于 gVisor 的开发者们对操作系统的安全做了一些研究,他们发现,大多数对操作系统的成功的攻击都是来自于不常用的系统调用的。
这个很容易理解,因为不常用的系统调用,它的实现路径一般都是比较老的路径,也就是说这些部分的开发一般不是太积极,只有很少的开发者来维护,那些热门路径上的代码要更安全一些,因为那些代码被 review 的次数比较多。所以 gVisor 的设计就是让应用对那些并不常用的 Syscall 的访问根本就到不了操作系统层面,而只在 sentry 里就把它处理掉。
从 sentry 访问宿主机的,只使用那些被验证过的、比较成熟、比较热的路径上的系统调用,这样的话,安全性就会比原来看起来好很多。我们现在 Syscall 是原来的 1/5,但是被攻击的可能性是并不到 1/5 的。
其次,他们发现,一些经常被攻击的系统调用需要把它隔离出来,比如 open(),就是打开文件的那个操作;
在 Unix 系统里面,大部分东西都是一些文件,所以 open 可以做太多的事情了,大部分的攻击都是通过 open 来进行的。gVisor 的开发者就单独地把 open 放到了一个独立的进程里面去实现,这个进程叫做 Gofer。一个独立的进程实际上是更容器被 seccomp、被一些系统的限制、一些 "capbility drop"来保护。Gofer 可以做更少的事情,可以用非 root 去执行,如此一来整个系统的安全性就被进一步地被提高了。
最后,sentry 和 Gofer 都是用 Go 语言来实现的,不是用传统的 C 语言实现的。
Go 语言本身是一个内存更安全的一个实现,因此整个 gVisor 就更不容易被攻击,更不容易发生一些内存上的问题。当然,Go 语言在有些地方还是不够太系统级的,gVisor 的开发者也坦言,他们为了做这件事情,也对 Go Runtime 做了很多调整,并把这些东西也反馈回给了 Go 语言的社区。
可以说 gVisor 的架构很漂亮,有很多开发者跟我坦诚,他们其实很喜欢 gVisor 的架构,觉得这个更简单、更纯粹、更干净。当然了,虽然它的架构很漂亮,但重新实现一个内核这件事情也只有 Google 这样的巨头能做得出来,类似的可能还有微软的 WSL 1。而且这个设计是比较超前的,它其实存在一些问题:
首先,sentry 并不是 Linux,所以在兼容性方面与 kata 这样的方案比起来还是有一定的差距的。这个没有办法,但是对于特定应用来说,这个可能并不是问题;
其次,对于当前的系统调用的实现方式,还有 CPU 的指令系统来说,我们从应用去拦截 Syscall,再把这个 Syscall 送给 sentry 去执行,这个过程本身是有相当大的开销的。在一定场景之下,gVisor 是可以有更好的性能的。但是,在大部分的场景之下,gVisor 的性能仍然是比不上 kata 这样的解决方案的。
所以短时间之内,gVisor 这样的解决方案并不能成为一个终极的解决方案,不过它可以适应一些特定的场景,并且它也带来一些启示性。我觉得这个启示性对未来的操作系统、CPU 指令集的发展都可能会有一些作用。而且我相信,在未来,不管是 kata 还是 gVisor,都会有一个演进,我们期待着最后会有一个公共的解决方案来统一地解决应用的执行问题。
五、安全容器:不止于安全
安全容器的名字虽叫安全,但是它提供的是一个隔离性。它的作用是不止于安全的。
安全容器通过隔离层让应用的问题——不管是来自于外部的恶意攻击还是说意外的错误,都不至于影响主机,也不会在不同的 Pod 之间相互影响,所以实际上,这个额外的隔离层,它所带来的影响不只是安全,还有其它的方面。它对于系统的调度、服务质量,还有应用信息的保护都是有好处的。
我们说传统的操作系统容器技术是内核进程管理的一个延伸,容器进程本身是一组相关联的进程,对于宿主机的调度系统来说,它是完全可见的,一个 Pod 里的所有容器或进程,同时也都被宿主机调度和管理。这就意味着,如果你有一个大量容器的环境,宿主机本身内核的负担就会很重,在很多实际环境中已经可以观察到这个负担带来的开销了。
尤其是现在计算机技术的不断发展,一个操作系统会有大量的内存,大量的 CPU,几百 G 的内存都是可以见到的。在这个情况下,如果分配的容器数量很多,调度系统就会有非常沉重的开销。在采纳安全容器之后,在宿主机上就看不到这些完整的信息了,这个隔离层同时承担了一些对隔离层上面应用的调度,于是在主机上面就只需要调度这些沙箱本身,降低了宿主机的调度开销,这也就是它为什么会提高调度效率的原因。
提高调度效率的同时,它会把所有的应用彼此隔离起来,这样就避免了容器之间、容器和主机之间的干扰,提高了服务质量。从另外一个方向来看,我们做安全容器的初衷是为了保护宿主机不受到容器内恶意或者有问题的应用的影响,反过来,作为一个云来说,我们有可能会面对有恶意的攻击,所以也是保护我们自己。
同时用户也不愿意让我们过多地去访问用户的资源,用户需要使用资源,但它并不需要我们看到它的数据。安全容器可以把用户运行的东西完全封装在容器里,这样的话可以让主机的运维管理操作并不能访问到应用的数据,从而把应用的数据保护在沙箱里,不需要去碰到用户数据。如果我们要访问用户数据,作为一个云的话,那就必须得让用户给你授权,这个时候,用户不确定你是不是有什么恶意的操作,如果我们的沙箱封装得很好的话,那也就不需要额外的对用户授权的要求,这对于保护用户的私密性是更好的。
当我们把目光看向未来的时候,可以看到,安全容器不仅仅是在做安全隔离,安全容器隔离层的内核相对于宿主机的内核是独立的,专门对应用服务,从这个角度来说,主机和应用的功能之间实际上是一个合理的功能分配与优化。它可以展现出很多的潜力,未来的安全容器,可能不仅仅是隔离性能开销的降低,同时也是在提高应用的性能。隔离技术会让云原生基础设施更加完美。
六、本文总结
本文的主要内容就到此为止了,这里为大家简单总结一下:
现在,所谓“安全容器”是指一种容器运行时技术,为容器应用提供一个完整的操作系统执行环境(常常是 Linux ABI),但将应用的执行与宿主机操作系统隔离开,避免应用直接访问主机资源,从而可以在容器主机之间或容器之间提供额外的保护;
Kata Containers 是一个使用虚拟化来提供隔离层的开源安全容器项目,完全兼容 Kubernetes 等云原生生态系统,项目托管在OpenStack Foundation,由蚂蚁金服和Intel共同领导;
gVisor 是一种利用进程级虚拟化技术实现的安全容器技术,由 Google 开发并开源,用 Go 语言实现了一个用户态的兼容内核;
最后,安全容器提供的隔离性不止是安全中的一环,也可以提供性能、调度、管理方面的隔离。