软件供应链安全听上去像是那些设有专职安全团队的大机构才需要操心的事,而正是这种定位把人带偏了。一个只维护少数几个服务的小团队,通常也依赖着几百个包,其中没有一个是团队里有人真正读过的。这些包在构建时从团队并不掌控的包仓库里拉取,然后在存放着部署凭据的机器上执行安装脚本。
暴露面并不随公司规模等比增长。它随依赖数量和构建自动化程度增长,而小团队往往前者更多、后者更少有人盯着,处境比那些公开讨论这件事的大机构还要吃紧一些。
令人不适的算术: 你的应用大概有十几个直接依赖,以及几百个间接依赖。这十几个是你自己挑的。剩下的不是,你一个都没读过,而其中任何一个只要执行安装脚本,拿到的权限就和你的构建进程完全一样。这才是真实的攻击面,而在你亲手写下的那份依赖清单里,它根本看不出来。
供应链风险从安装那一刻开始
危险的时刻通常不是运行代码,而是安装代码。
包管理器允许在安装过程中执行脚本,这意味着一个被入侵或本身就带恶意的包,会以执行安装命令那个人的权限直接运行。在开发者的机器上,那是他本人的凭据。在构建流水线里,那是部署密钥,后一种情况要糟糕得多。
真实发生的事故,绝大多数可以归入三种攻击形态。
typosquatting,也就是仿冒近似包名。 攻击者发布一个名字只比热门包差一个字符的包,然后守在那里,等着有人把安装命令敲错。实施成本极低,而规模一大就一定有人中招。
合法包的账号被盗。 一个被广泛使用的包,维护者的凭据遭窃,随后被塞进有害内容再发布出去。这一类最难防,因为无论是包名还是下载量,都看不出任何异常。
依赖混淆。 攻击者用你内部包的名字在公共仓库里发布同名包,而配置错误的解析器偏偏优先选择公共仓库而不是你的私有仓库。它完完全全是配置问题,也完完全全可以避免。
这三种攻击都不需要有人专门盯上你,这正是关键所在。它们是机会主义的,而且可以无限铺开。
小规模下真正划算的控制
把锁文件提交进仓库,并且从锁文件安装。 锁文件锁定的是精确版本和对应的哈希。如果改从清单文件安装,构建时就会重新解析出新版本,构建因此不可复现,上游的一处改动也会未经任何评审就抵达生产环境。请使用在锁文件对不上时直接失败的安装命令,而不是那个会悄悄把锁文件改掉的命令。
能关掉安装脚本的地方就关掉。 许多生态都支持这个开关,而大多数包本来就不需要安装脚本。这是在不引入任何额外工具的前提下,你能拿到的最大一次暴露面削减;而少数因此跑不起来的包,恰恰是你应该心里有数的那几个。
把漏洞告警自动化,然后做分诊。 扫描器产出的结果远超一个小团队能处理的量,而失效的方式并不是漏掉某条告警,而是因为绝大多数与自己无关,索性把所有告警都无视掉。请先筛出那些确实能从你自己的代码里被触达的问题,只处理这一部分。一条没人看的队列,比根本没有队列还要糟糕。
构建工具同样要钉死版本。 容器镜像、运行时版本,以及流水线调用的那个动作或插件,全都是依赖。只要引用的是可变标签而不是不可变的摘要,你的构建就会在你不知情的情况下发生变化。
把构建用的凭据和其他一切分开。 一条有权部署的流水线,不应该同时握着能做更多事情的凭据;而一次被入侵的构建,也不应该有能力碰到生产数据。
SBOM 能告诉你什么,不能告诉你什么
软件物料清单,简称 SBOM,列出的是你的应用里到底装了些什么,它的价值就在于能迅速回答一个问题:这件事影响到我们了吗?
这个问题以前要花好几天才答得上来。当一个被广泛使用的组件被曝出漏洞时,手里有一份最新清单的组织几分钟就能给出答复,而没有清单的组织要花一整周去翻查。这个差距就是全部理由,也正是各方推动采用 SBOM 的原因:它被当作基础实践,而不是什么高阶控制措施。
它做不到的,是让你变得安全。清单终究是清单,不是防御。它不会告诉你其中列出的漏洞在你的代码里是否可达,不会告诉你它在你的配置下是否真的要紧,也不会告诉你是否已经有东西被入侵了。生成一份然后归档了事的团队,只是多了一份文档,而不是多了一项控制。
请从构建过程里生成它,这样它描述的才是真正发出去的东西,而不是清单文件所声称的东西。每一个发布版本都要留下与之对应的那一份。同时也要接受一点:它的全部价值,都体现在你被追问时能多快给出答案。
小团队真正吃亏的地方
很少是因为某个稀奇古怪的包被入侵,而是因为一些再普通不过的事。
留在仓库里的密钥,删掉之后仍然留在历史记录里,推送后几分钟内就会被自动化扫描器找到。一个停留在三年前的依赖,已经有公开的利用代码,却没有可行的升级路径,因为升级被一拖再拖,拖到本身变成了一个项目。权限过大的构建流水线,来自派生仓库的合并请求能以本不该拥有的权限运行。没有人在看,于是入侵长期存在,因为没有任何一条告警最终送到了活人面前。
这个结论一点也不好看:在实际工作中,把依赖保持在最新状态,就已经构成供应链安全的绝大部分。团队当成可选项的软件维护成本,指的正是同一件工作,而拖延它,会把一次例行升级变成一场附带现成利用代码的紧急事故。
一个与规模相称的起点
对一个小团队来说,按这个顺序做:先把密钥从仓库里清出去,凡是暴露过的一律轮换。接着把锁文件提交进仓库,并严格从中安装。打开依赖告警,改成每周集中分诊而不是随时盯着。把构建工具钉死在不可变的引用上。把流水线权限收缩到必需的最小范围。等这些都到位之后,再为每个发布版本生成一份 SBOM。
这个顺序在没有专职安全团队的情况下,也能覆盖住现实中会遇到的威胁,而且每一步花的是几个小时,不是几个星期。再往上的那些控制,比如来源证明、可复现构建、签名产物,价值都是实打实的,但它们属于基础已经稳定运转之后的阶段。
Mecanik 会在应用程序安全分析服务中审查并加固构建流水线。我们查出来的东西几乎从来不是什么精巧的入侵,而是仓库里躺着的一个令牌,以及一个自从被选进项目那天起就再没人更新过的依赖。
常见问题
为什么安装包才是有风险的那一刻? 因为包管理器允许脚本在安装过程中运行,而且是以执行安装命令那个人的权限运行。在开发者机器上,那就是他本人的凭据;在构建流水线里,那就是部署密钥。这段代码根本不需要被你的应用调用,就已经能造成破坏了。
什么是依赖混淆? 这是一种攻击:攻击者用你内部包的名字在公共仓库里发布同名包,而配置错误的解析器优先选择了公共仓库而不是你的私有仓库。它不需要有人专门盯上你,而且只要把解析器配置改对,就能完全避免。
SBOM 实际上能带来什么? 它能对一个问题给出快速答案:这件事影响到我们了吗?有一份最新清单,几分钟就够;没有的话,要花一整周去翻查。它不会告诉你列出的漏洞在你的代码里是否可达,也不会告诉你是否已经有东西被入侵,所以一份生成之后就归档的 SBOM,只是文档而不是控制措施。
我应该把锁文件提交进仓库吗? 应该,并且要严格从锁文件安装,用那个在对不上时直接失败的命令,而不是会把它改掉的命令。从清单文件安装会在构建时重新解析出新版本,让构建变得不可复现,也会让上游的一处改动未经任何评审就进入生产环境。
小团队应该从哪里开始? 先把密钥从仓库里清出去并轮换所有暴露过的凭据,把锁文件提交进仓库并严格从中安装,打开依赖告警并每周分诊,把构建工具钉死在不可变引用上,收缩流水线权限,然后为每个发布版本生成一份 SBOM。每一步花的都是几个小时,不是几个星期。
评论