容器和安全计算模式 (seccomp) 是沙盒原语,可为虚拟机 (VM) 提供更轻量级的替代方案。在这里,我们介绍它们之间的差异,以及我们如何在 Figma 中使用两者来实现安全隔离。
Seccomp是安全计算模式的缩写,可以限制程序允许进行的系统调用。
快速回顾一下,与虚拟机不同,容器隔离发生在操作系统 (OS) 层,并且通常依赖于主机的操作系统功能来实现安全隔离,例如命名空间、cgroup 或特权删除等内核功能。
虽然图像处理和数据解析等功能是 Figma 等应用程序的核心,但它们带来了安全团队必须减轻的风险。完全防止安全漏洞成本高昂且不现实,因此我们使用服务器端沙箱(也称为工作负载隔离),我们将在此处详细介绍。我们已经将虚拟机作为沙箱原语进行了探索,因此我们将把注意力转向容器和 seccomp,并解开我们在 Figma 实现容器级安全隔离的一些方法。
我们将像评估虚拟机一样评估容器隔离,主要考虑两个安全问题:
容器内的恶意作业是否会爆发并影响主机系统?
即使不能,它是否可以使用容器的权限访问其他系统或以其他方式造成损害?
容器到主机的攻击面
三个组件——运行时实现、运行时可用的操作系统原语和接口以及运行时配置——通常构成容器逃逸的攻击面。默认情况下,容器不会自动成为安全沙箱,因为提供的隔离级别很大程度上取决于这三个因素。内核漏洞、运行时实现中的错误和/或运行时错误配置可能允许恶意工作负载修改文件并在其主机上执行代码。
Docker是一个运行容器应用程序的平台。
例如,在基于 Linux 的系统上,Docker使用“runC”运行时,它可以利用诸如命名空间、cgroup、权限下降、seccomp、通过SELinux或AppArmor进行强制访问控制等内核功能来提供隔离属性。Docker 的运行时配置管理如何使用和配置这些功能。
与商用虚拟机解决方案不同,容器让用户承担更大的责任来正确配置所需的隔离级别。对安全配置的更多控制也意味着犯错误的空间更大。(正如他们所说,“具有强大的功能......”。)虽然许多容器系统具有比以前更安全的默认配置,这可能适用于某些沙箱用例,但用户(您)有责任检查并进行必要的更改。
受损容器
与虚拟机一样,将可能受到损害的工作负载放置在容器中不一定足以保证安全。您需要加强容器配置以防止主机接管,并且整个容器基础架构应限制受损容器的影响。例如,一种方法可能涉及没有安装网络设备、凭证或访问其他数据的容器。在这种情况下,您可以将容器放入自己的隔离网络中,并使用编排系统将输入安全地传递到容器中并通过受控通道使用输出。
seccomp 安全模型
nsjail是一个命令行工具,它利用 Linux 命名空间、功能、文件系统限制、cgroup、资源限制和 seccomp 来实现隔离。firejail是一个集合所有者用户 ID (SUID),允许您对不同的进程进行沙箱处理。
正如我们在服务器端沙箱简介中所分享的,仅 seccomp 沙箱背后的想法是,许多程序执行纯计算,因此根本不需要动态访问文件系统或进行网络调用。对于这些程序,我们可以仅依靠 seccomp 来限制它们所需的系统调用,理想情况下限制它们可以采取的操作来分配内存、生成输出和退出。Seccomp 是一种强大的隔离原语,用于许多常用应用程序,例如 Android、Chrome 和 Firefox。事实上,seccomp 可以与容器化相结合,提供强大的、多层的以沙箱为中心的系统,例如nsjail和firejail。
系统调用接口是程序和内核之间的接口。
对于仅 seccomp 方法,攻击面由两个元素组成:内核的 seccomp 实现和系统调用 (syscall)接口,以及允许的系统调用列表。因此,虽然攻击面比虚拟机管理程序或容器化原语更容易推理,但仅限 seccomp 的沙箱具有重大限制。您有责任确定您的程序是否可以仅使用 seccomp 安全隔离以及哪些系统调用是安全的,同时仍然使您的程序能够正常运行。
通常,您需要重写程序以不需要危险的系统调用或应用其他沙箱原语来提供更强大的防御。识别哪些系统调用是安全的、哪些是危险的可能会变得很复杂。例如,seccomp 的原始版本只允许 exit、sigreturn、read 和 write 调用,其理论是真正的“纯”计算只需要这些功能即可运行,并且这种减少的内核攻击面足以防御。不幸的是,工程师认为纯计算的许多事情需要更多的系统调用。
特别是,编写无法分配堆内存的代码对于许多开发人员的工作模型来说是一个相当大的变化,并且语言运行时、核心库或跟踪帮助程序通常希望了解当前的系统时间。这些操作看起来无害,但允许的系统调用的每次增量增加都会导致需要考虑额外的内核攻击面。在 Figma,我们限制程序将输出写入已打开的文件描述符、退出、分配内存并获取当前时间,从而避免系统中复杂或覆盖较少的区域,例如文件系统、网络和套接字管理,或者钥匙扣。
工程考虑
在服务器端沙箱的介绍中,我们向您和您的团队分享了一些问题,供您和您的团队考虑不同的沙箱解决方案及其权衡。让我们通过这个镜头来评估容器和 seccomp。
环境
与虚拟机相比,开箱即用的容器往往提供更受限制的环境。有些程序可能根本无法运行或者可能需要修改才能正常运行;例如,如果他们有专门的硬件、操作系统或渲染要求。基于 seccomp 的方法要求工作负载在具有内核支持的 Linux 系统中运行。
安全性和性能
虽然容器和 seccomp 呈现出安全性和性能的权衡,但直接比较比虚拟机更加复杂和细致。
集装箱
gVisor是一个与 Linux 兼容的开源沙箱。
总体而言,比较虚拟机与容器化提供的安全隔离级别并不简单。例如,您可能会认为虚拟机管理程序的攻击面通常小于操作系统内核的攻击面,或者讨论近年来允许容器逃逸的内核漏洞利用的数量。另一方面,存在允许虚拟机突破而无需攻击虚拟机管理程序本身的错误,而gVisor等新技术可以通过在操作系统内核和容器进程之间插入自己的强化内核来减少容器可用的攻击面。虽然虚拟机总体上应该通过很少的配置选项提供非常强大的主机-客户隔离,但通常您仍然需要自带编排并应对更高的性能开销。相比之下,容器化还可以提供相当强大的主机-客户隔离,并可以提供细粒度的控制来实现这一点,但需要您正确配置所有内容。
此外,与虚拟机一样,安全隔离的细粒度也会影响使用容器的性能影响。它还会影响您需要构建的容器编排量。一般来说,设置和拆除虚拟机的性能成本应该比虚拟机低。例如,nsjail 的启动时间通常为几分之一秒、几十到几百毫秒。然而,启动时间仍然很长,并且在容器内初始化语言运行时可能需要更长的时间。
塞康普
同样,比较虚拟机和容器化与仅 seccomp 提供的安全隔离级别也并不简单;seccomp 在很大程度上取决于内核的系统调用接口和您配置的白名单。例如,允许“ptrace”系统调用在较旧的 Linux 内核版本上并不安全,因为它可能允许沙箱逃逸。总的来说,如果只允许最少的系统调用,就可以实现极强的隔离。由于系统调用过滤成本低廉,因此仅使用 seccomp 的沙箱的性能开销比基于虚拟机管理程序或基于容器的解决方案要低得多。
开发成本和摩擦
容器和 seccomp 需要大量的开发和调优成本,尤其是与虚拟机相比。
集装箱
采用基于容器的沙箱有两个主要挑战:配置和编排。
与虚拟机相比,为基于容器的沙箱正确配置安全边界可能需要更多的技术专业知识和实验。有一个很长的最佳实践清单可供遵循,例如删除权限、将进程放入新的 Linux 命名空间以及限制挂载点等等。重要的是要了解每种防御措施的作用以及它们提供什么保证(以及它们不提供什么保证)。
安全地对复杂程序进行沙箱处理需要深入了解程序的功能以及它如何与系统资源交互(例如它们读取和写入哪些文件系统位置,以及它们需要哪些功能)以及如何应用可用的容器功能以最大限度地减少可用资源攻击面。您可能需要通过反复试验来迭代以获得功能性且适当安全的配置,这可能会增加大量的工作量。
安全配置只是沙盒采用成本的一部分。对于虚拟机和容器,您可能需要构建自己的系统来管理工作池、安全地输入工作负载数据并安全地提取输出。
塞康普
与通常可用作嵌入式沙箱解决方案的虚拟机和容器相比,采用仅 seccomp 的沙箱需要更多的自定义。工程成本很大程度上取决于 seccomp 是否适合对程序进行沙箱处理。要创建 seccomp 允许列表,您需要了解程序可以进行的所有可能的系统调用,或者更典型的是,通过在代表性输入语料库上使用“strace”等工具运行程序来根据经验构建此列表,以执行所有可能的代码路径。
目前, seccomp 的一个显着限制是它只能在顶层过滤系统调用参数,而不能取消引用指针参数或执行其他更复杂的参数处理。因此,seccomp 无法根据打开的资源有选择地过滤某些系统调用,例如“openat”,因为它需要取消引用指针参数。反过来,您可能必须重写程序以更适合 seccomp,以便在执行处理用户输入的危险工作之前执行所有有风险的系统调用。(我们将在下面进一步讨论如何在 RenderServer 中执行此操作的示例。)当然,如果您拥有源代码,则重写程序只是一种选择,并且对于许多商品程序来说可能不可行。
仅 seccomp 方法的另一个好处是,您通常可以直接调用沙盒程序,而不必担心额外的编排或管道。
维护和运营费用
容器和 seccomp 都占用运营资源,安全团队之外的工程师可能需要更新程序或为其开发新功能。虽然这对于容器来说不是什么问题,但 seccomp 允许列表可能很脆弱。如果新的程序行为没有显着增加攻击面,则可能需要将更多的系统调用添加到允许列表中;如果新的系统调用确实添加了不可接受的攻击面,则可能需要进行大量重写。
此外,监视和调试仅限 seccomp 的沙箱可能特别棘手。内核日志将指示进程何时被 seccomp 终止以及哪个系统调用导致了问题,而无需提供更多上下文。调试和重现该问题可能会很费力且令人沮丧。它可能失败的原因有很多:很少使用的代码路径中被忽略的系统调用;最近引入的需要新系统调用的行为;系统更改——例如升级的库或内核;测试环境和生产环境之间的架构差异;即使是非常罕见的程序被恶意利用的情况。对 seccomp 进行良好的持续集成 (CI) 测试可以帮助尽早发现一些但不是全部的问题。
figma" class="css-vsobh0" style="box-sizing: inherit; cursor: var(--cursorText); margin: 80px 0px 40px; font-size: min(5vw, 4.5rem); font-variation-settings: "wght" 750, "wdth" 70; font-feature-settings: "ss01"; line-height: 1.04; letter-spacing: 0.005em;">Figma 基于容器的沙箱
在 Figma,我们现在将nsjail用于适合容器级安全隔离的用例。但到达那里是一段旅程,我们必须评估我们自己的一系列权衡。
使用 nsjail 沙箱
nsjail 的主要用例是隔离 RenderServer,这是一个用 C++ 编写的 Figma 编辑器的服务器版本,我们用它来提供缩略图等功能。RenderServer 具有一组复杂的功能,利用图形处理单元 (GPU) 加速来执行渲染,并用于多个后端服务。当我们第一次考虑如何沙箱 RenderServer 时,我们简要考虑了Docker,但很快意识到它会增加大量的前期开发工作和复杂性。例如,我们需要创建一个新服务,将 RenderServer 二进制文件沙箱化在安全的 Docker 配置中,创建一个编排系统来管理该服务,并重新构建各种服务以对 RenderServer 服务进行网络调用,而不是调用直接二进制。单独的服务可能是一项合理的长期投资,但不允许我们根据不断变化的需求灵活地探索不同的选择。相反,我们采用 nsjail 作为嵌入式解决方案,以便我们可以集中精力根据我们的需求安全地配置它。
此配置利用 nsjail 提供的所有沙箱功能。对于每个用户请求,nsjail 都会在新用户、pid、挂载和网络命名空间中启动一个新的 RenderServer 进程,并且没有网络访问权限。Nsjail 还将限制 RenderServer 进程只能访问特定的文件系统挂载点,例如输入文件、库和输出文件夹。当然,nsjail 使用 seccomp-bpf 来强制执行严格的系统调用列表。
当我们第一次将 RenderServer 的沙盒版本投入生产时,良好的监控和功能标志控制可以实现顺利部署。在初始部署中,我们发现作业错误数量虽少,但数量仍然较多,经过调查,我们发现许多错误与包含大图像的输入文件相关,并导致输出文件大小恰好为 1 MB。非常可疑!事实证明,我们没有足够仔细地阅读 nsjail 文档,并且默认rlimit_fsize
设置为 1 MB。经过一番快速 PR 后,我们的沙盒渲染服务器运行正常。
毫不奇怪,我们还必须在推出期间多次更新我们的 seccomp 允许列表。我们在复杂的 RenderServer 代码库(大规模服务用户流量)中遇到了非常罕见的代码路径,这是我们在测试或内部使用期间没有遇到的。
仅使用 seccomp
最终,我们针对某些不需要 GPU 加速的 RenderServer 用例探索了一种仅使用 seccomp 的沙箱方法,以降低性能开销和操作 nsjail 的复杂性。一开始的主要挑战是,我们无法仅使用 seccomp 充分限制 RenderServer 的文件系统访问,因为文件 I/O 分布在 RenderServer 中各种功能代码路径的许多地方。
下面是一个说明问题的基本示例:假设我们要使用 RenderServer 帮助用户将 Figma 文件导出为 SVG。在这种情况下,必须进行沙盒处理的危险步骤是 Figma 文件的处理,该文件可能包含想要破解 Figma 的攻击者植入的恶意输入。RenderServer 和各种用内存不安全语言(主要是 C++)编写的第三方库的组合负责处理。RenderServer首先会完成预览图像的生成,然后再将其写入输出文件。如果 RenderServer 因图像处理步骤而受到损害,我们希望在图像处理之前应用 seccomp 过滤器,以防止受损害的 RenderServer 进程执行恶意操作,例如打开和读取系统上的其他敏感文件。但是,如果我们这样做(通过配置 seccomp 过滤器来阻止像 RenderServer 这样的系统调用),openat
RenderServer 将停止正常工作,因为 seccomp 也会在打开其他文件(例如其指定的输出文件)时终止它。
我们重构了 RenderServer 以重新排序所有打开的文件,以便它们在对潜在危险的用户输入进行任何图像处理之前发生,这是一项艰巨的工作。最终,重构允许我们通过 libseccomp 添加限制性 seccomp 过滤器,以防止 RenderServer 除了仅读取和写入允许访问的资源之外执行任何其他操作。这个新版本的 RenderServer 更容易测试和调试,并且它的运行速度比之前使用 nsjail 的迭代要快得多。但是,它也给希望向 RenderServer 添加新功能或改进的工程师带来了限制。这将 RenderServer 锁定为单线程应用程序,意味着我们无法在运行时动态加载字体或图像。
基于容器的沙箱和 seccomp 沙箱可以成为应用程序安全隔离的轻量级解决方案,但需要在开发和维护开销方面进行重要权衡。