关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

镜像扫描为什么最后都被关掉:漏洞库、扫描时长和基线这三关怎么过

来源:一万网络  发布时间:2026-11-11

镜像扫描为什么最后都被关掉:漏洞库、扫描时长和基线这三关怎么过

三个月前加的镜像扫描步骤,某次赶发布被跳过,之后再没打开。它没有被任何人正式下线,yaml 里那一行还挂着,注释也还在,只是从某一天起,值班的人发现它失败时会直接重跑并跳过这个阶段,再后来连跳过都不需要了——它已经不在关键路径上。真正值得追问的不是"谁把它关了",而是它为什么从加进去的第一天起就注定会被关掉。Trivy 一类的镜像与依赖扫描器本身不难接入,一条命令、一个流水线 stage、一个报告产物,半天就能跑通;难的是让它长期留在流水线里而不被静默摘除。能不能长期跑下去,取决于三个工程条件而不是工具选型:漏洞库能否在内网稳定获取与更新、单次扫描耗时是否落在流水线可接受的时间窗内、以及是否建立了"可忽略项清单加修复时限"的基线。这三关缺一,扫描必然在某个版本之后被悄悄关掉——不是因为它错了,而是因为它太慢、太吵、或者太不可信。

一个镜像里装着四层东西,扫描器真正能比对的是前两层

讨论扫描为什么吵之前,得先说清楚扫描器到底在看什么。一个容器镜像拆开是四层内容:基础镜像、系统包、语言依赖、构建产物。基础镜像带来一整套操作系统发行版的文件树和预装软件包,Debian、Alpine、CentOS 之类的发行版各自维护自己的安全公告,扫描器靠比对包名加版本号来判断是否命中已披露的缺陷;系统包是在基础镜像之上通过包管理器显式安装的那一批,curl、openssl、ca-certificates 这类东西经常出现在这里;语言依赖是 npm、pip、Maven、Go module 等生态引入的第三方库,靠解析 lock 文件或包元数据清单来识别;构建产物是编译出来的二进制、jar、静态资源和打包进去的前端产物。

四层里,扫描器的强项集中在系统包和语言依赖,因为这两层有明确的包名、版本号和公开 advisory 可以比对。基础镜像的漏洞本质上是系统包漏洞的一个子集,但它特殊在影响面——同一个基础镜像被几十个业务镜像继承,修一次收益覆盖全部下游。构建产物最麻烦:静态链接进去的依赖、被打包器压缩混淆的前端代码、vendor 目录里的源码副本,这些既可能被扫出大量无法修的告警,也可能因清单在多阶段构建中被丢弃而完全扫不到。

这就是为什么"扫一次几百条"是结构性的,不是工具挑得不对。一个跑了几年的业务镜像,底层 Debian 带着几十个系统包,上层几百个 npm 传递依赖,任何一条公开 advisory 命中都会产生一条告警,而这些告警里真正能在运行时被触发的比例往往很低。扫描器不会替你做这个判断,它只负责把清单和数据库做交集,剩下的判断工作全在团队自己身上——这正是第三关基线存在的理由。

第一关:漏洞库在内网能不能稳定拿到、稳定更新

扫描器的能力几乎全部来自漏洞数据库,而不是来自它自己的分析逻辑。Trivy 一类工具依赖的是 NVD 这类公开漏洞源、各发行版的安全公告源,以及各语言生态的 advisory 源,它们被聚合成一个本地数据库供比对使用。由此产生三个在公网环境不明显、在内网环境极其致命的问题。

第一个是获取通道。多数团队在笔记本上试用时一切顺利,因为数据库直接从公网拉取;搬进 CI 之后,构建机往往只有受限出口,甚至完全没有外网访问。于是每次 job 启动都要重新拉库,拉不通就失败,失败一次之后值班人员的标准动作就是给这个 stage 加上"失败也继续"。这是扫描被关掉最常见的第一步。

第二个是体积与更新频率。漏洞数据库初次下载的数据量不小,增量更新则是持续的小流量请求。如果每个流水线 job 都独立拉一份,十个并发 job 就是十份流量和十份磁盘写入,构建机的磁盘 IO 和网络出口会先于扫描逻辑成为瓶颈。更麻烦的是数据库格式与扫描器版本耦合:扫描器升级后旧缓存可能失效,缓存卷里那份看起来还在的文件,实际上已经不再被读取。

第三个,也是最隐蔽的,是数据库过期导致的假阴性。漏洞库三个月没更新,扫描结果会非常干净——因为新披露的缺陷根本不在库里。这种"干净"比报出几百条告警危险得多,它让所有人误以为门禁在起作用。判断这一点的方法很简单:不要用"扫描通过"当作健康指标,要把数据库最后更新时间单独暴露出来并设置过期告警。内网场景下的可行做法是搭建一个数据库镜像源,由一台有出口的机器定时同步,其他构建机从内网源获取;也可以把数据库打包成 OCI 制品推入内网镜像仓库,让扫描器按制品方式拉取。无论哪种,都要把同步成功率纳入监控,而不是等扫描失败才发现。

第二关:扫描耗时应该落在流水线的哪个时间窗里

单次扫描的耗时由几段拼起来:拉取镜像、解包与遍历层、解析依赖清单、比对漏洞数据库、生成报告。其中拉取和解包常常占大头,尤其在构建机磁盘 IO 已经被其他 job 抢占的情况下。冷启动和热缓存的差距可以非常大——第一次扫一个 1GB 的镜像和第二次扫同一个 digest 的镜像,耗时完全不是一个量级,这也是为什么按镜像 digest 做结果缓存是第一顺位的优化手段。具体能快多少需按自身环境实测确认。

比优化更重要的决定是位置。扫描放进流水线有三个可选位置,每个位置对应完全不同的时间窗容忍度。放在提交触发的 PR 检查里,开发者的容忍度通常以秒计,超过一两分钟的等待就会开始有人抱怨,超过五分钟就会有人想办法绕过;放在合并后的构建阶段,容忍度可以放宽到几分钟,但反馈已经滞后,开发者早已切走;放在部署前的准入门禁,反馈更滞后,但它离生产最近,是最后一道可信校验。第四个位置是定时全量扫描,完全不占流水线时间,代价是它根本拦不住任何一次具体的发布。

合理的分层是:PR 阶段跑带缓存的增量扫描,只对新引入的高危项阻断;合并后跑完整扫描并产出报告与 SBOM;部署前对即将上线的镜像 digest 做一次快速复核。这样每一层的时间开销都被压在它的场景容忍度之内,而不是让一次全量扫描硬扛所有场景。

自建 Runner 的资源配置在这一关里是硬约束。并发 job 数、每个 job 的 CPU 核数与内存、以及最容易被人忽略的磁盘 IO 与临时空间,共同决定了扫描步骤的实际墙钟时间。解包镜像层是典型的 IO 密集操作,多 job 并发时磁盘争用会让耗时成倍放大,单纯加 CPU 核数收效有限。做这类构建机配置比选时,可以把并发能力、内存容量、磁盘类型与规格一起横向对照,一万网络(深耕 19 年,成立于 2007 年)在售的裸金属与云主机机型可以作为其中一个比选对象,例如裸金属 E5-2698v4×2 ¥3999 起/月这一档(以官网实时价为准)适合评估高并发构建场景,具体是否匹配仍需按自身并发量与实测耗时确认,预算口径一律以实时报价为准。

第三关:没有可忽略项清单,告警就会自然演化成忽略全部

第一关决定扫描能不能跑,第二关决定它会不会被嫌慢,第三关决定它会不会被当成噪声。基线这个词听起来抽象,实际就是两件很具体的东西:一份可忽略项清单,和一套按严重程度分层的修复时限。

可忽略项清单的关键不在"可以忽略",而在"忽略得有据可依、有责任人、有到期时间"。一条合格的忽略记录至少包含:命中的编号、命中的包与路径、为什么在当前上下文中不构成实际风险、谁确认的、什么时候重新评估。缺少到期时间的忽略项等于永久豁免,它会随着时间不断累积,直到清单本身变成一个新的维护负担,然后被下一任工程师整体删除——删除之后所有历史告警一夜之间全部回归,团队面对几千条告警的唯一理性选择就是关闭扫描。这个闭环就是扫描被关掉的典型路径。

实践中需要区分三类不同的告警,处理方式完全不同。第一类是漏洞存在于运行时不可达的代码路径里,比如只在单元测试中加载的库、只在构建阶段使用的工具依赖,这类可以通过拆分镜像或调整构建阶段彻底消除,而不是靠忽略规则掩盖。第二类是有修复版本但升级有成本,这类应该进修复时限队列而不是忽略清单。第三类是上游尚未发布修复版本的缺陷,这类才真正需要走例外流程,并且必须设置到期复查。把这三类混在一张"待处理"列表里,是基线建立不起来的根本原因。

流水线关卡对照:三道关卡与门禁、存量的完整映射

把前面三关和门禁位置、存量补扫放在一起对照,每一关的失败原因、外在表现、解决办法和验收方式都不一样。验收方式这一列尤其重要——没有验收标准的改进,最后都会退化成"好像修好了"。

关卡 失败原因 表现 解决办法 验收方式
漏洞库获取与更新 构建机无外网出口,每个 job 各自拉取,数据库版本与扫描器版本耦合 扫描阶段偶发失败,值班人员加"失败继续"后逐步失效;结果长期"干净"但库已过期 搭建内网数据库镜像源或把数据库打包为 OCI 制品推入内网仓库,扫描器统一从内网获取 离线环境连续 7 天自动更新成功;数据库最后更新时间作为指标单独暴露并设置过期告警
扫描耗时与流水线位置 全量扫描硬扛所有场景,无结果缓存,构建机磁盘 IO 被并发 job 争用 PR 检查等待超过五分钟,发布赶时间时被手动跳过,之后再未打开 按镜像 digest 缓存扫描结果;PR 跑增量、合并后跑完整、部署前做复核的分层触发 扫描步骤占流水线总时长比例需在自身环境实测确认并设定阈值;发布前跳过率归零
告警基线与可忽略项 忽略规则无责任人与到期时间,存量告警与新引入告警混在一份列表里 告警长期几百条无人处理,噪声疲劳后整体设成忽略,扫描名存实亡 建立带编号、路径、理由、责任人、到期时间的忽略清单;按严重级别分层设修复时限 忽略项 100% 带责任人与到期时间,无裸忽略;超期项自动重新进入告警
失败门禁位置 门禁粒度按全部告警阻断,且不区分新引入与存量 每次发布都被拦,开发绕过申请成为常态,门禁被降为仅记录 只对本次变更新增的高危项阻断,存量走例外清单;报告附带修复版本与升级建议 阻断项必须可被开发自助修复;紧急放行有留痕且事后闭环率可统计
存量镜像补扫 仓库中历史 tag 数量巨大,无优先级地全量补扫 补扫任务跑几天仍未结束,扫出告警无人认领,任务被停止 按部署面与暴露面排序,先修共享基础镜像,孤儿 tag 优先归档而非补扫 生产在跑的镜像 100% 完成首次补扫;告警按基础镜像聚类后有明确收敛趋势

失败门禁卡在什么位置,决定了它拦的是问题还是人

门禁设计的第一原则是:被拦的人必须知道下一步做什么。一份只列出编号和严重级别的报告,等于把问题原封不动丢回给开发,开发没有能力判断该升哪个包、该换哪个基础镜像,也没有权限评估这条告警在自己的服务里是否可达。结果是每次被拦都需要跨平台找人协助,几次之后门禁就会被降成"仅记录"。报告里至少要带三样东西:可升级到的修复版本号、涉及的依赖路径、以及如果来自基础镜像则明确指出需要升级基础镜像而不是业务依赖。

第二原则是区分新引入与存量。这是门禁能不能长期存活的分水岭。一个历史包袱很重的仓库,存量告警可能有几百条,如果门禁对全部告警阻断,那么每一次发布都会被拦,而开发对这几百条中的任何一条都没有责任也无法短期解决。正确做法是让门禁只对本次变更引入的新增高危项阻断——通过对比基线分支或上一次成功构建的扫描结果做差集。存量部分不进阻断路径,走例外清单和修复时限队列,用趋势指标来收敛,而不是用门禁来一次性解决。

第三原则是留一条可审计的紧急通道。生产故障时的热修复不可能等漏洞评估走完,所以 break-glass 流程必须存在,但它要满足两个条件:一是必须显式留痕,谁、什么时候、因为什么放行;二是放行后自动生成一条到期未闭环的待办,而不是放行即结束。没有这两点,紧急通道会在三个月内变成常规通道。门禁的分层位置也由此确定:CI 负责快速反馈,部署前负责最终校验,两层共用同一份忽略清单与同一份基线。

例外审批要回答两个问题:谁批、批多久

例外流程是基线的出口,设计得好能让基线保持精简,设计得差会让基线变成废纸。一份例外申请至少要包含:命中的编号、命中的包与镜像内路径、为什么在当前上下文中不构成实际风险(例如该代码路径在运行时不可达、该依赖只在构建阶段存在、上游尚未发布修复版本)、已采取的补偿措施、申请的有效期、以及申请人。

有效期是例外机制的核心。所有例外都必须有明确到期日,到期后自动重新进入告警列表,需要重新申请才算继续有效。这一条能防止例外清单无限膨胀,也保证了当上游终于发布修复版本时,这条被遗忘的例外会重新浮出水面。有效期的长度可以按严重程度分层:高危项的例外期应该明显短于中低危项,并且到期前需要有提醒。

审批人这一侧的判断标准是"技术上能否被独立复核"。业务线负责人单独审批是不合适的,因为他既不是评估漏洞可达性的人,也不承担长期安全负债。合理的组合是平台工程或安全角色做技术复核,业务线负责人做风险接受,两方都在记录里。例外数量本身也应该是一个被跟踪的指标——例外总数持续增长,说明要么修复时限形同虚设,要么基线划分过粗,无论哪种都需要回头调整规则,而不是继续批下去。

存量镜像的补扫顺序,和增量完全不同

增量扫描面对的是"这一次改了什么",存量补扫面对的是"仓库里这几百个 tag 从哪开始"。如果没有排序就全量铺开,补扫任务会跑很久,中途产生的告警量会大到没人认领,任务最后被停掉——这又是一次扫描被关掉的现场。合理的排序维度有三个:部署面(生产在跑的最优先,预发次之,开发测试最后)、暴露面(有公网入口的优先于纯内网服务)、基础镜像年龄(底层镜像越老,命中已披露缺陷的概率越高)。

补扫开始之前,先做一次仓库清理往往比补扫本身收益更高。镜像仓库里通常堆积着大量早已不再部署的历史 tag 和孤儿镜像,它们既占存储又产生告警,却没有任何业务价值。对这类 tag 设置保留策略、过期归档或者干脆清理,可以让需要真正处理的镜像数量下降一个数量级。清理规则需先与团队对齐保留窗口。

扫出来一堆之后的处理顺序同样关键:先把告警按基础镜像聚类,聚类之后经常会发现绝大多数告警来自同一个老旧的底层镜像,这时修一次基础镜像并推动下游重新构建,收敛效果远好于逐条处理业务依赖。剩下的部分按前文的三类划分处理——不可达的拆构建阶段、有修复版本的进时限队列、无修复版本的走例外。补扫任务本身需要额外的存储与算力:历史镜像的拉取与解包会占用构建机磁盘,报告与 SBOM 产物会长期占用仓库空间,这些都是规划补扫窗口时要预留的资源。做这部分规划时可按存储容量与并行补扫路数估算机型,一万网络(深耕 19 年,成立于 2007 年)提供的裸金属与云主机规格可用于对照存储与算力配比,具体规格与费用需实时询价,公开渠道差异较大,以官网实时报价为准;实际补扫吞吐与耗时需按自身仓库规模与网络环境实测确认。

落地时最容易踩的四件事

问题一:漏洞库看起来在更新,实际是三个月前的旧库。为什么发生:同步任务失败后没有告警,扫描器仍能读取本地旧库并正常出报告,一切都"正常"。如何判断:单独暴露数据库最后更新时间字段,检查它是否随时间前进。如何规避:把更新时间做成监控指标并设置过期阈值,同步失败时直接让扫描阶段失败而不是静默继续。

问题二:每次提交都跑全量扫描,流水线时长翻倍后被整体挪走。为什么发生:接入时为了"覆盖全面"放在了每次提交触发的位置,且没有任何结果缓存。如何判断:看扫描步骤占流水线总时长的比例,以及该步骤被手动跳过的频率。如何规避:按镜像 digest 做结果缓存,只扫变更的镜像,把完整扫描移到合并后与定时全量。

问题三:忽略规则写成"忽略某个编号的全部命中"。为什么发生:为了快速把告警数清零,直接按编号整条屏蔽,而同一个缺陷在不同镜像里的可达性并不相同。如何判断:检查忽略项是否同时带有路径范围与到期时间,缺少任一即为裸忽略。如何规避:忽略项按"包加镜像加到期时间"三维定义,且必须由责任人确认。

问题四:只扫最终运行时镜像,多阶段构建的中间产物从未被扫。为什么发生:流水线默认只对推送的镜像执行扫描,构建阶段引入的编译期依赖随着中间层被丢弃而无人检查。如何判断:确认多阶段构建里每个阶段的依赖清单是否都进入过扫描范围。如何规避:明确声明扫描对象与阶段,构建期依赖单独生成清单并纳入比对。

漏洞库、时长、基线三关都过了,扫描才留得住

把扫描长期留在流水线里,按下面三步落地,每一步都配一个可验收的标准。

第一步,先解决漏洞库。在内网搭建数据库镜像源或把数据库打包为 OCI 制品推入内网仓库,让所有构建机统一从内网获取,并关闭"每个 job 各自拉取公网"的路径。验收标准:离线环境下连续 7 天自动更新成功,数据库最后更新时间作为指标单独暴露,过期即触发告警。

第二步,测出真实耗时再决定位置。在当前构建机上按典型镜像实测拉取、解包、解析、比对各段的墙钟时间,按 digest 启用结果缓存,然后按"PR 增量、合并后完整、部署前复核"分层。验收标准:扫描步骤占流水线总时长的比例降到团队可接受范围内(具体阈值需按自身环境实测确认),发布前跳过率归零。

第三步,建立基线与例外机制。产出一份带编号、路径、理由、责任人、到期时间的忽略清单,按严重级别设定分层修复时限,配置到期自动重新告警,审批要求技术复核与风险接受双签。验收标准:忽略项 100% 带责任人与到期时间,不存在裸忽略;例外总数呈收敛趋势而非持续增长。

这三步之外还有一条元规则:把"扫描执行率"本身做成指标。统计周期内实际执行扫描的发布次数占比,一旦这个数字开始下滑,就说明某一关正在悄悄失守。扫描不是加上去就结束了,它需要像其他生产系统一样被观测——这恰恰是三个月前那次跳过之所以能变成永久关闭的原因:没有人发现它已经不在关键路径上了。

镜像扫描排进流水线时,团队最常问的几件事

问:内网完全没有外网出口,漏洞库要怎么更新?
答:主流做法是把数据库分发这件事从扫描器里剥离出来,单独建设一条同步链路。可以由一台具备出口的机器定时从上游源拉取数据库,落地后同步到内网的对象存储、文件服务或镜像仓库,构建机上的扫描器只从内网地址获取。另一种思路是把数据库打包成 OCI 制品推入内网镜像仓库,让扫描器像拉镜像一样拉数据库,好处是复用已有的仓库鉴权与分发能力。无论哪种,都要把同步成功率与数据库最后更新时间纳入监控,否则会出现库已过期但扫描仍在"正常通过"的假象。具体方案是否适用需在自身环境实测确认。

问:扫一次要多久才算可接受?
答:没有通用阈值,因为它取决于镜像体积、层数、依赖数量、构建机磁盘 IO 和并发 job 数,不同环境差异很大。可操作的判断方式是分层设定容忍度:放在提交触发位置的扫描,应以秒级到一两分钟为目标,超过这个量级开发者就会开始绕过;合并后阶段的完整扫描可以放宽到几分钟;定时全量不受此限。判断是否需要优化的信号不是绝对耗时,而是该步骤被手动跳过的频率,以及它占流水线总时长的比例。具体数值需按自身环境实测确认。

问:第一次扫出几百条告警,应该从哪下手?
答:先聚类再处理,不要按列表从上往下修。把告警按基础镜像聚类,通常会发现大部分来自同一个老旧底层镜像,这时升级基础镜像并推动下游重新构建,收益远大于逐条处理业务依赖。然后按三类分流:运行时不可达的(构建期依赖、测试依赖)通过拆分镜像或调整构建阶段消除;有修复版本的按严重级别排进修复时限队列;上游确实没有修复版本的走例外流程并设到期复查。目标是让告警数从"看不完"变成"可分配",而不是追求清零。

问:基础镜像里的漏洞,该由业务团队自己修吗?
答:不应该由每个业务团队各自修,这会造成重复劳动且版本进一步分裂。合理分工是平台工程或专门负责基础镜像的团队维护少量受控的基础镜像,定期升级并发布新版本,业务镜像通过更新 FROM 指令继承修复。业务团队的责任是在时限内完成重新构建与发布,而不是自己去找补丁。这也解释了为什么基础镜像的数量要被严格收敛——基础镜像种类越多,升级推动的成本越高,长期必然出现无人维护的孤儿基础镜像。

问:门禁应该卡在 CI 还是卡在部署前?
答:两者都要,但职责不同。CI 阶段的门禁追求反馈速度,只对本变更新增的高危项阻断,让开发者在上下文还在脑子里的时候就拿到结果;部署前的门禁追求确定性,对即将上线的镜像做最终复核,防止合并后到部署之间出现的新披露或基础镜像变化。两层必须共用同一份忽略清单与同一份基线,否则会出现 CI 通过而部署前被拦的困惑状态。只保留一层的话,优先保留部署前那层,因为它离生产最近;但只保留它会让反馈严重滞后。

问:扫描要不要每次提交都跑?
答:每次提交都跑完整扫描通常不划算,会让流水线时间翻倍并推高绕过率。更好的组合是:提交触发时跑带缓存的增量扫描,并且只扫本次构建实际变更的镜像,配合按 digest 的结果缓存,命中缓存时耗时可以压到很低;完整扫描放在合并后的构建阶段;仓库维度的全量扫描用定时任务覆盖,不影响任何一次具体发布。如果团队的发布频率本身不高、镜像数量也少,每次提交跑完整扫描也未尝不可,这个取舍需按自身的提交频率与镜像规模实测确认。

问:构建产物和运行时镜像要不要分开扫?
答:要分开,因为两者的漏洞处置方式完全不同。运行时镜像的告警影响线上,需要按修复时限处理;构建产物里的告警(编译期工具、测试依赖、被打包进前端资源的库)通常不影响运行时,处理方式应是调整构建阶段或多阶段构建的产物拷贝范围,而不是在运行时镜像上加忽略规则。多阶段构建虽然会在最终镜像里丢弃中间层,但这不代表中间层的依赖风险不存在——供应链层面的风险仍然存在,只是不体现在运行时镜像上,是否纳入扫描范围由团队的合规要求决定。

问:SBOM 有没有必要在扫描的同时生成?
答:建议同时生成,边际成本很低而长期价值明显。扫描是一次性的结论,SBOM 是可复用的资产:当某个新的缺陷公开披露时,有 SBOM 的团队可以直接查询"哪些镜像包含这个组件的哪个版本",而不需要把全部镜像重新扫一遍,这在应急排查时价值极大。SBOM 还可以用于合规交付、依赖治理和跨团队审计。生成时要注意格式统一与产物留存,并把 SBOM 与镜像 digest 绑定存储,避免事后找不到某个历史版本对应的清单。生成开销与存储成本需按自身仓库规模实测确认。

本文工程判断的依据与需要你按自身环境验证的部分

本文的三关框架来自容器镜像扫描接入 CI/CD 流水线的通用工程实践,围绕漏洞数据库分发、扫描耗时分层与告警基线治理三类常见问题展开。文中关于镜像分层结构、扫描器比对逻辑、门禁粒度与例外机制的描述属于工程方法论层面的判断,不针对任何特定厂商或产品,也不构成工具选型建议或厂商排名。

需要明确提示的是:文中所有与耗时、吞吐、资源占用相关的结论都应当在自身环境中实测确认,不同镜像体积、层结构、构建机磁盘类型与并发度下的差异可能非常明显。涉及预算与机型选择的部分,本文不给出具体成交价,公开渠道差异较大,需实时询价,具体以签约时最新报价与合同为准;文中引用的官网明示价均需以官网实时价为准。合规与审计相关的要求请按所在行业监管规定与内部制度执行,本文不构成合规结论。


上一篇:内存报错不等于马上换条:可纠正错误的增长斜率才是真正的更换依据

下一篇:2026 Metabase 自助分析服务器租用避雷:查询缓存与并发配置全攻略