2019 年 11 月(好像已经过了半个世纪),我们宣布将命名空间隔离添加到 NGINX Unit 中。在本文中,我们将探讨该隔离机制最近新添的另一个选项,即隔离
对象的 rootfs
选项。
正如在宣布推出 NGINX Unit 1.18.0 的博文中所提到的,rootfs
选项支持您将任意目录指定为应用的系统文件根。因此,您可将应用作为轻量级的按需式容器配置和运行,从而提高其安全性,将其与底层操作系统相隔离,并提高基础架构的细粒度。
但是,这并不意味着可以随意为之。
注意事项和技术问题
或许最值得一提的是,rootfs
特性仅在支持绑定挂载或 nullfs
文件系统的 Linux 和 Unix 系统上可用。
此外,如果隔离
对象定义了新的挂载
命名空间,为提高安全性,NGINX Unit 会使用 pivot_root
系统调用而非 chroot
。因此,使用 rootfs
的另一个技术要求是 NGINX Unit 的主进程要有系统管理员权限,特别是 CAP_SYS_ADMIN
,以便进行所有必要的系统调用。
实际上,这两个注意事项意味着 主进程需要作为 root
运行。虽然很有可能您已满足这项要求,但这并不是百分百不变的:在某些安装中,NGINX Unit 的主进程可作为其他系统用户运行。作为 root
运行这一要求并不会给所运行的应用带来额外风险,因为主进程不处理客户端连接或运行应用代码, 所有这些都由使用非特权证书运行的单独进程处理。
下面就让我们了解下 rootfs
可派上用场的一些场景。
保护应用
虽然显而易见,但还是有必要说一下:整个隔离
对象的主要目标是使应用在物理上无法超越您设置的限制。首次引入这一概念时,隔离对象无法限制文件系统访问,但是 rootfs
选项弥补了这一空白。
假设我们有一个 Python 应用遭到入侵,现在可以注入任意代码(通过静态文件上传等方式)。首先,让我们从攻击者的角度看下有哪些可乘之机:
此代码会扫描系统的 setuid
可执行文件,这些可执行文件可被进一步利用,但其实是被禁止的。如果可以诱骗任何此类可执行文件提供对敏感数据的访问权限,则系统将受到严重破坏。然而,此类配置错误总会定期发生,现在就让我们从基本设置的角度看下典型的配置错误是如何发生的:
这样配置后,受感染的应用将生成以下结果:
$ curl http://localhost/find/?path=/usr/bin
/usr/bin/passwd
/usr/bin/fusermount
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/umount
/usr/bin/pkexec
/usr/bin/sg
/usr/bin/cp
/usr/bin/atrm
...
如您所见,我们的初始扫描收获颇丰:我们通过精心配置的 /usr/bin/cp 可执行文件成功获得了攻击向量。接下来,我们假设使用先前用过的注入方法,提取一些有价值的数据:
通过运行此代码,我们可以窥视一些敏感信息:
$ curl http://localhost/exfiltrate/?file=/etc/shadow
root:$6$QF7EX8XQ4BnLFVo/$f3hqo1vdWqK77kEuY4NOKsvgP1.XBtcO4fOND78IV/jP1i6/PtG/RHWZAqL3PQ3AVvwXwgBUbmAeOVtYDSg2o/:18471:0:99999:7:::
...
现在,让我们将 rootfs
添加到组合中以避免再次遭到攻击。下面是我们的新配置:
受感染的应用现在如何处理同一攻击的两个阶段?让我们来看看:
$ curl http://localhost/find/?path=/usr/bin
遍历不会生成任何结果,因为系统目录未映射到新的文件系统根目录。
$ curl http://localhost/exfiltrate/?file=/etc/shadow
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
“嘿,没那么快!”按照预期,我们现在无法访问敏感文件:rootfs
选项禁止该应用访问外界。我们也无法访问备受觊觎的 /etc/shadow 或随意遍历系统目录。该应用已被限制在其沙盒目录之内。
但是,如果攻击者知道该应用会被沙盒化并有应对之策,该怎么办?长话短说,攻击者可通过不少臭名昭著的方法逃脱 chroot
环境,并且经过精心筹备后可将此环境用于攻击。但是,NGINX Unit 通过提供一种使用 pivot_root
系统调用而非 chroot
的方法解决了这一问题。您只需启用 mount
命名空间以及 rootfs
选项:
最后,rootfs
的另一项简洁特性是您可以将语言特定的依赖项自动映射到新文件系统。因此,启用 rootfs
后,我们无需执行任何操作便可使 rootfs
指令生效。它们仍然是这样:
请注意,如果 rootfs
仅更改了文件系统根目录,那么第二个 import
指令将变为无效,标准模块将无处可寻。另外还请注意,我们无需采取任何措施即可使 Flask 虚拟环境成功运行,一切都是自动实现的。不幸的是,这种映射目前仅可用于 NGINX Unit 支持的某些语言,即 Java、Python 和 Ruby。
谈到映射,让我们简要探讨下可通过 rootfs
解决的另一个问题。
处理全局依赖项
如上所述,NGINX Unit 启用了完善的内部机制,以确保在 rootfs
将语言特定的依赖项映射到新文件系统根目录之后,这些依赖项仍可供您的应用使用。但是其他依赖项呢?
通常,当应用依赖于可置于系统上任何位置的自定义库或模块时,working_directory
选项和 environment
对象以及诸如 root
之类的语言特定选项将支持您在自定义依赖项之间按需切换。
而且,某些语言本身就提供了一个工具集,可通过虚拟环境或其他类型的版本控制来操纵这种依赖性。NGINX Unit 实际上就利用了这点,在本地支持 Python 虚拟环境。但是,如果您的应用具有固定的全系统依赖项,并且这些依赖项必须位于绝对的预定义路径中,则情况可能会变得一团糟:并排使用不同版本的依赖项可能会变得很复杂,即便不是完全没有可能。在这种情况下,您可以使用 rootfs
来实施基本的运行时切换机制。
设想以下 PHP 应用(尽管您也可以使用其他语言执行这一操作):
现在我们简单了解下依赖项 module.php。假设我们有两个版本(没什么复杂的,只是表明两者之间的不同而已):
现在,我们将应用的两个相同副本放置在 /www/data/a/ 和 /www/data/b/中,并应用以下配置:
请注意,应用的 root
选项与 rootfs
值相关。实际上,当使用 rootfs
时,这适用于所有基于应用路径的选项。
利用此配置,curl
命令将生成以下结果:
$ curl http://localhost
Implementation A, legacy: How do you like this?
接下来,我们要将 module.php 切换到版本 B。这时我们无需费力重新安装或启动另一个容器(通常后者更现实),而是只需运行以下命令:
$ curl -X PUT -d '"/www/data/b/"' --unix-socket /var/run/control.unit.sock http://localhost/config/applications/ab_app/isolation/rootfs/
这将更新 rootfs
设置(注意 URL 中的 config API 路径),而 NGINX Unit 配置的所有其他部分均保持不变。现在,curl
查询做出了不同的响应:
$ curl http://localhost
实施 B,您认为怎么样?
这个示例虽然简单,但清晰地表明了 NGINX Unit 可如何帮助您避免创建和维护多个虚拟机或容器的繁琐工作,从而高效应对全系统依赖项的不同组合。您可以复制和回滚您的应用或语言运行时在预定义路径中的标准库和自定义模块变体,只需更改 NGINX Unit 配置中的单个设置即可。遗憾的是,当存在分层、隐藏和间接依赖项时,此功能可能会表现得有些失常,目前我们正在积极解决这一问题。
结语
在本文中,我们探讨了可从新 rootfs
特性中获益的几种使用场景。我们相信,这已经充分展示了 NGINX Unit 从简单但强大的 Web 服务器转变成强劲的轻量级容器化引擎的过程。我们不希望让人我觉得我们言过其实。实际上,我们还有许多功能要实现,例如文中提到的语言特定的依赖项映射。
同时我们深知,要想提供一套真正通用的隔离功能,我们还需要添加另一项特性,即目录绑定。 为了全面兑现 NGINX Unit 容器化承诺,我们需要支持将任意目录挂载到根及可信文件系统中的任何位置。 即将发布的版本将提供这一支持,敬请期待!
再次诚邀您查看我们的路线图,您可以在这里了解到您最关注的特性何时上线,并对我们的内部计划进行评价。欢迎您随时在 GitHub 存储库中提问,并分享您的改进建议。
NGINX Plus 订阅用户可免费获得 NGINX Unit 支持。立即下载 NGINX Plus 30 天免费试用版,或与我们讨论您的使用场景。