沙箱和工作负载隔离

2020-07-31 02:27:13

工作负载隔离使一个服务中的漏洞更难危及平台的其他部分。它有很长的历史,可以追溯到20世纪90年代的qmail,我们普遍认为它是一件好的、有用的事情。

尽管有过多的隔离选择,但在我为科技公司提供咨询的那段时间里,我了解到最常见的隔离机制是“一无所有”。这就说得通了!大多数服务都是其部署环境的单一承租人,或者至少是逻辑体系结构的核心,因此没有什么可以有意义地将它们与之隔离。由于隔离可能代价高昂,而且通常安全资源不足,因此精心设计的遏制方案往往不是优先考虑的重点。

当你主持别人的东西时,这种逻辑就会被抛诸脑后。Fly.io是Docker容器的内容交付网络。我们通过将应用程序放置在靠近其用户的位置来实现快速应用程序;我们通过在世界各地的一系列数据中心运行裸机服务器,并将它们与全球WireGuard网格结合在一起来实现这一点。玩Fly.io是非常容易的--一位数的分钟让你头脑清醒,与其谈论它,我只建议你拿个免费账户试一试。

同时,我要一口气说出几种不同的隔离技巧。现在,我将为您破坏这个列表:我们使用的是Firecracker,这是Amazon的Lambda和Fargate服务背后的虚拟化引擎。但是我们选择“爆竹”的解决方案空间很有趣,所以你会听到它的。

人们喜欢说“chroot不是安全边界”,但这当然不是真的,它本身就不是很强大。Chroot是最早的沙盒技术。

Chroot最有趣的问题是它是如何实现的:在内核进程表中,每个struct proc(我是在BSD上提出的)都有一个指向其当前工作目录和根目录的指针。当您尝试cd到“..”时,根目录是“强制的”;如果您当前的工作目录已经是根目录,内核将不会让“..”到下面去。但是,当您调用chroot(2)时,您不一定要更改目录;如果您位于新根的“上方”,内核在路径遍历中将永远不会看到那个新根。

当然,真正的问题是内核攻击面。我们还不需要变得可爱;就其本身而言,考虑到没有其他对策,chroot为您提供了ptrace、procfs、设备节点,当然还有网络。

通过不以“root”身份运行任何内容(但不是所有内容),您可以摆脱很多这样的问题。快速但重要的一点是:在现实世界的攻击中,您可以向攻击者让步的最重要功能是访问您的内部网络。出于同样的原因,SSRF漏洞(“意外的HTTP代理”)几乎总是游戏结束,尽管乍一看,它们看起来并不比未经检查的重定向可怕多少:您可以将内部HTTP请求对准某个目标,让攻击者执行代码。Chroot监狱中的网络访问就是这样,但要灵活得多。

这个问题将笼罩在我在这里写的几乎所有东西上,请记住这一点。

Chroot是现代沙箱中一个很受欢迎的组件,但它们中没有一个真正完全依赖它。

现在是1998年,您可以用来构建的唯一严肃的语言是C。您想要为一组用户接收邮件,或者验证并启动一个新的SSH会话。但这些都是复杂的、多步骤的操作,没有人知道如何编写安全的C代码;要想弄清楚这一点,还需要30年的时间。您假设您将在某个地方搞砸一个解析,然后付给RCE。但是你需要特权才能完成你的工作。

一种解决方案是:将服务拆分成多个较小的服务。为服务提供不同的用户ID。使用组ID连接服务。把代码弄得乱七八糟,这样最粗糙的东西就会出现在与其他服务连接最少的低特权服务中。尽可能少地保留需要特权的内容,如邮箱投递或设置登录用户。

不管它的作者如何评价他的设计,这种方法工作得很好。它不是万无一失的,但实际上它有相当好的记录。主要缺点是它需要花费大量的精力来实现;您的应用程序需要意识到您正在这样做。

如果您可以更改您的应用程序以适应沙箱,那么您就可以将Prisep带到更远的地方。OpenBSD通过“承诺”和“揭开”做到了这一点,它们允许程序逐渐降低获得内核的权限。这是一个比seccomp更好、更灵活的习惯用法,稍后会详细介绍。但是您没有运行OpenBSD,所以,继续。

人们喜欢说“码头不是安全边界”,但现在不再是这样了,尽管它曾经是这样的。

容器背后的核心思想是内核命名空间,它被扩展到其他内核标识符-进程ID、用户ID、网络接口。经过仔细配置,这些功能使程序看起来像是在自己的机器上运行,即使它与容器外的其他程序共享一个正在运行的内核。

但是,即使有自己的PID空间、自己的用户和组以及自己的网络接口,我们仍然不能让进程将处理程序路径写入/sys、重新引导系统、加载内核模块和创建新的设备节点,虽然这些问题中的许多都可以通过不以root身份运行来避免,但并不是所有的都可以。

系统安全人员花了近十年的时间在Docker上扣篮,因为这个简化的容器模型中存在所有漏洞。但现在已经没有人真的像这样经营集装箱了。

强制访问控制框架(您将看到AppArmor)提供系统(或容器)范围的访问控制列表。您可以阅读Docker的默认AppArmor模板的一个版本,以了解这修复了哪些问题;这是对名称空间本身弱点的简明描述。

系统调用过滤器让我们可以关闭内核特性;在2020年,如果您要过滤系统调用,您可能会使用seccomp-bpf来实现。

功能将“root”划分为一大堆子特权,确保几乎不需要授予任何程序超级用户访问权限。

例如,现代码头利用了所有这些功能。虽然不完美,但我认为Docker Security人员得出的解决方案是一个成功的故事。开发人员不会有意识地强化他们的应用程序环境,然而,在很大程度上,他们也不会特权运行容器,也不会给容器额外的功能,也不会禁用Docker默认实施的MAC策略和系统调用过滤器。

在docker之外监禁一个进程可能会更容易;googler构建了迷你监狱和nsjar,Cloudflare有了“沙箱”,还有“fire jear”,它在某种程度上针对浏览器之类的东西进行了调整,systemd将为您完成其中的一些工作。使用哪种工具是个喜好问题;nsjar有不错的BPF UX;Firejar可以与AppArmor进行互操作。它们中的一些可以预加载到不合作的进程中。

使用命名空间监狱,我们已经达到了当前最流行的工作负载隔离端点。您可以做得更好,但是您将要处理的攻击开始变得微妙起来。

被监禁的应用程序环境的一个限制是,它们往往是在容器范围内应用的,或者至少是在进程范围内应用的。在大容量情况下,为每个作业分配一个进程可能会很昂贵。

如果您放宽了运行普通Unix程序的要求,您可以在没有细粒度的每进程安全模型的情况下获得监狱的一些好处。只需将所有内容编译成Javascript,并在V8隔离中运行它们即可。V8语言运行时对协同作业可以访问的内容做出了您可能信任也可能不信任的承诺。或者您可以使用Fastly的Lucet serverside WASM框架。

从安全的角度来看,假设您信任语言运行库(我想我信任),当您可以公开有限的系统界面(每个人都会这样做)时,这些方法很有吸引力,而如果您需要所有POSIX,那么作为一般设计,这些方法就不那么有吸引力了。

这里有一个我们还没有解决的问题:您可以设计一个复杂的、最小的系统调用白名单,删除所有权限,并切断大部分文件系统。但是,当Linux内核开发人员重新构造内核在将传递给系统调用的指针解引用时使用的内存访问检查时,有人忘了告诉维护waitid(2)的人,现在userland程序可以将内核地址传递给waitid和waitid随机内核内存。WAITID(2)是无害的,你不打算把它过滤掉,但你却在那里,筋疲力尽。

或者,这样如何:每次进程出现地址故障时,内核都必须查找后备存储器来解析该地址。因为这相对较慢,所以内核缓存。但它必须使这些缓存在进程中的所有线程之间保持同步,以便每个线程的缓存获得绑定到包含进程的计数器。例外:计数器是32位宽的,并且失效逻辑搞砸了,因此如果您滚动计数器,然后立即产生一个线程,然后让该线程再次滚动计数器,您可以取消同步线程的高速缓存,并让内核跟随过时的指针。

像这样的虫子是会发生的。它们被称为内核LPE。您可以通过加强系统调用和设备筛选器,并编译最小的内核(您并不总是真正使用IPv6DCCP)来缓解其中的许多问题。但其中一些问题,如Jann Horn的缓存失效错误,您无法通过这种方式修复。您对它们的关注程度取决于您的工作量。如果您只运行自己的应用程序,您可能不会太在意:利用此缺陷的攻击者已经在您的系统上安装了RCE,因此可以访问您的内部网络。如果您正在运行其他人的应用程序,您可能会非常在意,因为这是您的主要安全屏障。

如果名称空间和过滤器构成“监狱”,那么gVisor就是来自“囚徒”的村庄。如果我们只是重新实现Linux的大部分,而不只是过滤系统调用,会怎么样?我们运行普通的Unix程序,但是拦截所有的系统调用,而且在很大程度上,我们不是将它们传递给内核,而是自己满足它们。Linux内核有近400个系统调用。我们需要多少个才能有效地效仿其余的呢?GVisor只需要不到20个。

有了这些,gVisor基本上在用户端实现了所有的Linux。进程。装置。任务。地址空间和页表。文件系统。TCP/IP;整个IP网络堆栈,全部重新实现,在GO中,由原生Linux用户区作为后端。

这里的重点很简单:在GO代码中不太可能有常规的可利用的内存损坏缺陷。您可能会在C语言Linux内核中使用它们。Go足够快,可以在用户端可信地模拟Linux。如果没有必要,为什么还要公开C代码呢?

尽管这个计划很糟糕,但它工作得出奇地好;您可以相对容易地构建gVisor和runsc(它的容器运行时)。一旦您安装了runsc,它将为您运行Docker Containers。读完代码后,我有点不敢相信它工作得这么好,或者,如果是的话,它真的在使用我读过的代码。但我在代码库中散布了一堆恐慌电话,是的,所有这些事情都在发生。这真是太令人惊叹了。

严格来说,使用gVisor可能比使用调优的Docker配置更好,我非常喜欢它。最大的缺点是性能;您将看到两位数的低命中率,随着I/O负载的降低而降低。谷歌在GCE中规模化地运行这些东西;你可能也可以逃脱惩罚。如果您运行的是gVisor,那么您应该吹嘘一下,因为gVisor再一次非常简单。

如果您担心内核攻击面,但又不想在Userland中重新实现整个内核,那么有一种更简单的方法:只需虚拟化即可。让Linux成为Linux,并在虚拟机中引导它。

您几乎肯定已经信任虚拟化;如果虚拟机管理程序全面崩溃,那么所有AWS、GCE和Azure都会崩溃。而且Linux让超级链接变得非常简单!

这里的挑战主要在于性能。容器的很大一部分意义在于它们是轻量级的。从某种意义上说,服务器端隔离的目标是足够轻量级的虚拟化,以运行容器工作负载。

事实证明,这是一个合理的要求。虚拟机如此昂贵的主要原因是硬件仿真,它具有足够的保真度来运行多个操作系统。但是我们并不关心不同的操作系统;将我们的工作负载限制在Linux上通常是可以的。如果我们的虚拟机只能用简单的设备引导一个简单的Linux内核,那么我们的虚拟机能有多轻量级呢?

事实证明:相当轻便!所以我们有了Kata Containers,这是一个大公司支持的服务器端轻量级虚拟化项目,它出自Intel的Clear Containers(任务声明:“提出一个锁定到VT-x的容器方案”)。使用qemu-lite,kata消除了BIOS引导开销,用virtio的等价物替换了真实的设备,并积极缓存,并设法将引导时间缩短了大约75%。Kvmtool是另一种KVM运行时,它变得更加轻便。

第一个问题,也是整个虚拟化方法的真正大问题是,您需要裸机服务器来高效地执行轻量级虚拟化;您需要KVM,但不需要嵌套虚拟化。您可能不会仅仅为了获得一些额外的隔离而购买EC2金属实例。

第二个更具哲理性的问题是qemu和kvmtool是相对复杂的C代码库,我们希望尽量减少对它们的依赖。您可以合理地在gVisor和kata/kvmtool之间进行争论,gVisor使用内存安全的语言模拟Linux,kata/kvmtool使用不安全内存的虚拟机管理程序运行虚拟化的Linux。不过,他们可能都比被关起来的侏儒码头强。

轻量级虚拟化是AWS运行其功能即服务平台Lambda和无服务器容器平台Fargate的方式。但AWS在Rust中重新实现了QEMU,而不是信任QEMU(和艰苦的调优)。结果就是“鞭炮”。

爆竹是一种针对安全性进行了优化的VMM。真的很难夸大“鞭炮”是多么干净;“鞭炮”的论文吹嘘说他们已经在大约1400行的Rust代码中实现了他们的块设备,但在我看来,他们似乎在计算很多测试代码;你只需要花几百行Rust代码就能理解它。网络驱动程序将Linux分路设备适配到来宾Linux内核可以与之对话的virtio设备,在您命中测试之前大约有700行-这是锈迹,所以大约1/3的行是use-语句!真的很棒。

Firecracker(如果忽略C代码,还有kvmtool)的原因可能很简单,因为它们将系统复杂性推低了一层。它仍然在那里;您正在引导一个实际的、make-menuconfig的内核,在它所有的内存中-不安全的荣耀。但是,您是在虚拟机管理程序中执行此操作的,在“鞭炮”案例中,您实际上只担心KVM子系统本身的完整性。

我们还不是“鞭炮”的重要贡献者,但谈论这个项目仍然让人感觉很奇怪,因为它是我们产品的核心部分。这就是说:AWS的团队确实是以西区的方式做了这件事:

Firecracker VMM很小,易于阅读,并且有意实现运行Linux服务器工作负载所需的最少概念。

VMM seccomp-bpf本身只有大约40个系统调用],几个,包括基本的东西,如fcntl、socket和ioctl,具有严格的参数过滤器。

我认为,请记住,无论您的Linux系统隔离有多复杂,您需要减少的最重要的攻击面是暴露在您的网络中。如果您可以花时间对未分段的单VPC网络进行分段,或者进一步收紧默认的Docker seccomp-bpf策略,那么您的时间花在网络上可能会更好。

还要记住,当安全工具设计人员考虑隔离和减少攻击面时,他们通常假设您需要普通工具来运行,并且普通工具需要访问Internet;您的隔离工具不会进行开箱即用的网络隔离,例如,它们可能会保护您免受Video4Linux错误的影响。

在我看来,对于新的设计,当今主流选项的基本菜单是:

这些都是有效的选项!我要说的是:出于投资回报的目的,如果时间和精力是一个因素,并且如果我没有托管恶意代码,那么我可能会在购买容器策略之前调优nsbar配置。