长亭百川云 - 文章详情

问脉答记者问:为什么我们需要一套插件系统

VeinMind

55

2022-04-06

故事是这样开始的

当我们开发了一个容器安全工具,我们自然而然想在各种场景下复用这个工具。

举个栗子:我们写好了一个镜像后门扫描工具,会想在以下场景使用它:

  1. 本地扫描:当我们直接执行这个工具时,它能够对本地镜像进行扫描,并且输出镜像中的后门的检测结果;

  2. 操作授权:我们可以编写一个 Docker 授权插件,在拉取镜像和启动容器时,自动执行该工具进行镜像后门检测,并阻断风险操作;

  3. 远程扫描:我们希望使用这个工具对远程仓库中的镜像进行扫描,帮助我们发现存在后门的镜像,为后续的修复、溯源等操作提供支持。

除此以外,我们还能想到很多其他的使用场景,比如集成到 CI/CD 流程中进行扫描等。但我们不妨先以上述场景作为典型展开讨论。

一般情况下,为了方便开发和调试,我们开发的安全工具都会先支持本地扫描的功能。事实上我们大多数时候都“超量完成”了这个目标:

我们会花很多的时间去装饰这个工具的输出功能,可能这个工具一半的代码都花在了命令行配色、输出格式化和各式的报告生成等输出功能的优化上,这将增加一部分也许可以优化的工作量。

首先我们需要思考如何支持操作授权。

显然,我们可以在镜像后门扫描工具的基础上添加一个子命令,并把它封装为一个 Docker 授权插件,可是这要处理很多繁复细节。

因此我们会希望存在一个开源的 Docker 授权插件项目,它以子进程的方式调用我们的工具,并且依据工具的检测结果决定是否要阻断用户的操作。如果这样的 Docker 授权插件存在(或者若不存在我们可以自己做一个),那必然会减少我们很多的工作量。

在我们开始全网寻找或编写这样的 Docker 授权插件之前,不妨先思考一个问题:它应该如何获取我们的检测结果呢?

先考虑不对工具本身进行任何修改的方案

能不能直接读取工具的输出和报告,利用正则表达式等匹配出检测结果呢?不考虑使用正则表达式本身可能会产生的错误匹配,可以相信的一点是,如果放任不同的安全工具作者对输出结果进行自由发挥,一千个工具可能就有一千种输出模式。因此,要想基于正则匹配生成一个通用方案,只能让工具作者自己提供匹配其工具输出的正则表达式(我怀疑你在套娃),这有悖于我们提出讨论这一问题的初衷。

如果允许对工具本身进行一些修改,又会有哪些可行的方案呢?

既然我们依靠了子进程的方式执行工具,一个最简单的做法是判断命令的退出状态码。

举个栗子:状态码为 0 时认为当前检查没有发现威胁,否则认为发现了威胁并进行阻断。这确实是一个可行容易接受的方案,但仍然存在一些问题。

首先,状态码常用于表达程序是否成功执行并退出,那么当一个工具执行失败并返回非零状态码时(事实上很多语言的标准库在遇到运行时异常的情况下,会直接以非零状态码终止当前进程,并且何时终止进程和以何种状态码终止进程,大多数情况下都不受用户控制。),是否默认包含了应该阻断用户操作的意思呢?

有的用户认为所有安全工具都应该正常执行完毕且报告无威胁后才应该放行用户进行下一步操作,这样才能确保不放过任何一个安全问题;有的用户则认为仍在试用阶段的安全工具,如果不能正常工作则应该忽略其结果(比如我在试用一个安全工具时,运行过程中不断报错并返回非零状态码时,我想要忽略它而不是影响我的其他操作),只需要确保处于生产阶段的安全工具正常工作即可。

因此一刀切地处理非零状态码,并不一定能满足所有用户的使用需求。

其次,在发生阻断时,很多时候我们都希望把完整的阻断理由(比如在哪个路径下发现了哪种后门,哪个软件资产有哪个重大漏洞)呈现给用户,但是子进程的状态码是不包含这些信息的。而如果这类工具没有其他渠道报告具体的检测结果,我们只能去尝试解析工具的输出或报告(等等,听着怎么这么熟悉),并重蹈我们先前认为有悖初衷的正则匹配老路。

因此,若想以一种合理且可维护的方法提供检测结果给前文所述的 Docker 授权插件,工具应该输出一个计算机可读、格式预定义的检测结果,以供 Docker 授权插件进行解析和处理。

让我们接着思考如何支持远程扫描。

我们会希望编写的工具能提供一个子命令,接收远程仓库 URL 及其认证信息,而这个子命令只需要把相应的镜像 tar 包下载到本地,执行原来的扫描代码即可。

在只有单个安全工具需要执行时,这个思路很常见,但是更多时候我们会关心多一种威胁,因此会有同时执行多个检测工具的需求。在这种情况下,如果各个工具使用时都各自独立从远程下载镜像,除了会因为重复下载镜像 tar 文件(注意,镜像中一个 Layer 的大小经常在几百 MB 量级)占用大量网络带宽,重复扫描导致效率低下以外,部分公有仓库如 Docker Hub 还有下载频率限制。

如果你不是 Docker Hub 的付费用户,每个 IP 每 6 小时 100 次拉取的限制真的体验极差(别问我怎么知道的)

我们想到一个简单可行的解决方法:执行每一个工具之前,先把待扫描镜像下载到本地(不管是直接下载 tar 还是使用 Pull 指令),然后执行各工具进行扫描,待扫描完成后再移除镜像释放资源。

你是否已经注意到,这样就把针对远程仓库的扫描转化为本地扫描了,而进行扫描时只需要为各工具指定下载好的镜像,而无需让工具感知当前是否在扫描远程仓库。

这也就意味着工具即使只支持本地扫描,也可以在远程扫描的场景下复用。

同时为了方便使用,我们往往会编写一个远程扫描入口程序,接收扫描指令,并依此完成镜像下载、工具调用和镜像卸载的工作。(听起来有个框架了)

除了操作授权和远程扫描外,我们还能针对很多其他的使用场景展开实现细节上的讨论,并且总能发现在新的场景下,工具总需要作出或多或少的调整才能完美满足需求。事实上,使用场景的种类之多和各场景之间的差异之大,已经让在工具或者 SDK 中通过堆砌代码来应对成为不可能完成的任务。(有没有想到某张表情包)

因此,我们会通过抽象和适配等手段,想方设法将新的场景处理为已知的场景(如远程扫描中将扫描远程仓库处理为扫描本地仓库),这样才能达成在不同的使用场景下复用已有工具的初衷。

至此,一个插件系统的想法呼之欲出:如前文所述的 Docker 授权插件、远程扫描的入口程序,我们将这样的程序称为宿主程序(Host Program)。它们负责处理各使用场景下的具体细节,并将其转化为容器、镜像等具体实体的扫描问题;与之相对的,如前文所述的镜像后门扫描工具、镜像漏洞扫描工具,我们将这样的程序称为插件(Plugin),它们则能对容器、镜像等具体实体进行扫描,并发现其中存在的安全问题。

如上图所示,当用户有镜像或容器的安全检测需求时,仅需向 Host 发送一条命令,告诉 Host 具体的扫描对象,Host 会枚举当前存在 Plugins List 中的插件 Plugin B、C,依次调用 B、C 对指定对象进行扫描,获取扫描结果统一输出给用户。

因此宿主程序和插件属于一对多的关系,在实际使用中我们只需要先配置好了具体的宿主程序,然后依据我们所关心的安全问题插拔安全插件,最终获取我们期望的聚合的检测结果。

那宿主进程和插件之间是否是简单的单向的父子进程调用关系呢?

答案是否定的。在前文讨论如何支持操作授权时,宿主进程就需要收集插件输出的检测结果并处理,生成操作授权响应。在本文未讨论到的场景中,也存在其他采集检测结果并进行定制化处理的需求,如生成检测报告文件,生成 Syslog 并转发到 SIEM 等。同理,针对插件的日志输出,会希望支持进行采集和过滤等操作,以支持不同场景下的日志持久化、工具调试和界面展示等需求。

为了处理这些插件产生的、需要可定制化处理的数据,需要我们打通从插件到宿主进程的双向连接,并通过宿主进程向插件提供服务(Service)的方式,接受和处理插件产生的数据,还能进行在插件中获取和修改某些系统配置项等多种操作。

在问脉 SDK 中,我们除了针对容器安全相关实体设计了相应的 API 外,还设计并实现了一套插件系统。(就是我们上文讨论的方案)基于插件系统编写的安全工具只需一次编写,并在本地扫描的场景中验证,便可集成到不同使用场景下的宿主程序中得到复用;同样地,对于新的使用场景,只需基于插件系统编写新的宿主程序,便可复用现有的已经编写好的插件。

前往尝试一下吧:问脉 Tools libVeinMind: 问脉容器感知与安全 SDK

持续关注,你将会看到故事是这样继续的:基于 veinmind 系列出发详解如何编写插件 ......

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2