如何防止GitHub Actions中的依赖项出现问题/被恶意代码污染
你们的工作流程使用了 actions/checkout@v4 这一标签。目前,这个标签指向的是经过审核的版本代码;但明天,如果维护者或攻击者篡改了这个标签,它就会指向恶意代码。而你们的集成管道在运行时会使用 GITHUB_TOKEN 来访问这些代码。 第三方提供的 Actions 实际上属于依赖项。这种“浮动标签”的机制就类似于未固定的包依赖关系——如果引用地址发生变化,工作流程也会执行不同的代码,但你们的仓库本身并不会因此发生任何改变。 本文会教你如何通过将Actions绑定到具体的提交哈希值上来防止连续集成过程中的安全风险,包括验证这些绑定的准确性、限制允许执行的操作类型,以及利用Depe
你们的工作流程使用了actions/checkout@v4这一标签。目前,这个标签指向的是经过审核的版本代码;但明天,如果维护者或攻击者篡改了这个标签,它就会指向恶意代码。而你们的集成管道在运行时会使用GITHUB_TOKEN来访问这些代码。
第三方提供的Actions实际上属于依赖项。这种“浮动标签”的机制就类似于未固定的包依赖关系——如果引用地址发生变化,工作流程也会执行不同的代码,但你们的仓库本身并不会因此发生任何改变。
本文会教你如何通过将Actions绑定到具体的提交哈希值上来防止连续集成过程中的安全风险,包括验证这些绑定的准确性、限制允许执行的操作类型,以及利用Dependabot自动处理需要审核的更新请求。
适用对象:负责保障GitHub Actions工作流程安全的平台工程师及DevSecOps专家。
前提条件:
拥有仓库管理权限
使用
uses: org/action@ref语法配置工作流程
快速参考指南:
以下是你们需要掌握的内容:
将所有
uses:引用绑定到具体的提交哈希值上,而不是像@v4这样的浮动标签。在批准任何更新之前,务必验证所使用的提交哈希值是否与对应的发布版本相匹配。
在组织或仓库层面,制定明确的允许执行的操作列表。
对于需要审核的版本更新,务必启用Dependabot来进行自动化处理。
优先选择官方提供的或经过验证的第三方Actions;如有必要,可以在内部复制那些关键的操作流程。
在合并拉取请求之前,一定要检查这些绑定的有效性。
目录结构
为什么浮动标签会失效
GitHub Actions允许你使用分支名称、版本标签或提交哈希值来引用依赖项。然而,这些引用方式并不能提供同样的稳定性:分支随时都可能发生变化,而版本标签也可能在审核后被修改为指向其他提交。只有提交哈希值才能唯一地标识某个具体的代码版本。
下表对比了常见的引用格式,并说明了为什么使用可移动的标签会带来供应链风险:
| 引用格式 | 风险 |
|---|---|
@main |
当工作流开始时,会执行该分支上的所有代码 |
@v4 |
这个标签是可移动的,因此同一个标签可能会用来执行不同的代码 |
@v4.2.1 |
虽然更具体,但仍然属于可变的标签类型 |
@abc1234... (完整的SHA值) |
用于标识一个不可变的提交记录 |
前三种引用方式都是不正确的,因为这些标签名称并不能永久地确定GitHub应该执行哪段代码。而使用完整的SHA值就可以解决这个问题,因为只有当有人在仓库中修改了相应的引用信息时,工作流才会发生变化。
关键要点:持续集成管道也应该遵循与应用代码相同的依赖管理规范。
将操作与具体的提交SHA值关联起来
将某个操作与其对应的提交SHA值关联起来,意味着会用你审核过的那个提交的完整SHA值来替代该操作所引用的分支或版本标签。这样一来,即使原标签后来被修改了,GitHub也会继续使用你最初指定的SHA值来执行相应的操作。
第一个例子是错误的,因为这两个引用都使用了可变的版本标签。这样,工作流在未来可能会执行不同的代码,而你的仓库中却没有任何相应的更改:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
第二个例子是正确的,因为每个引用都使用了完整的40位SHA值。虽然注释中写的是易于理解的版本号,但GitHub实际上使用的是SHA值来进行匹配:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@4992456781334f2795ae9dd169f5041797d85689 # v4.4.0
你可以在该操作的GitHub发布页面上找到对应的SHA值,或者使用以下命令来获取它:
gh api repos/actions/checkout/git/ref/tags/v4.2.2 --jq '.object.sha'
请务必使用你实际审核过的那个提交标签。为了便于维护,可以在注释中写明该版本的详细信息。这些注释虽然对审核者有帮助,但并不属于安全控制机制的一部分。GitHub最终会执行由SHA值标识的那个提交记录。
千万不要使用缩写的SHA值。完整的40位SHA值能有效降低发生冲突或导致审核错误的可能性。
验证提交的SHA值
虽然完整的SHA值可以防止你在合并代码后标签位置发生变化,但在批准某个提交之前,仍然需要先验证其SHA值的准确性。发布标签和对应的提交记录应该一起进行审核。这个检查会对比你审核过的标签所对应的提交记录与工作流中记录的提交信息:
#!/usr/bin/env bash
set -euo pipefail
repository="actions/checkout"
release_tag="v4.2.2"
expected_sha="11bd71901bbe5b1630ceea73d27597364c9af683"
actual_sha="$({
git ls-remote "https://github.com/${repository}.git" \
"refs/tags/${release_tag}" "refs-tags/${release_tag}^{}"
} | awk -v tag="refstags/${release_tag}" '
$2 == tag "^{}" { print $1; found = 1 }
$2 == tag { fallback = $1 }
END { if (!found) print fallback }
')"
if [[ "${actual_sha}" != "${expected_sha}" ]]; then
printf 'SHA值不匹配:%s %s\n' "${repository}" "${release_tag}" >&2
printf '预期值:%s\n实际值:%s\n' "${expected_sha}" "${actual_sha}" >&2
exit 1
fi
printf '验证通过:%s@%s\n' "${repository}" "${expected_sha}"
这仅仅是一种版本验证机制,并不意味着某个仓库一定是可信赖的。请仔细阅读相关操作的来源信息,审查其权限设置,并记录下自己为何批准使用该依赖项。在需要更高安全性的环境中,可以在内部创建这些关键操作的镜像版本,然后通过常规的仓库管理流程来验证这些镜像文件的有效性。
为GitHub Actions启用Dependabot
Dependabot是GitHub提供的自动化依赖项更新服务。对于GitHub Actions来说,它会检查工作流中引用的依赖项是否为最新版本,并自动创建拉取请求来更新这些依赖项。这样一来,你就可以在一个便于审核的平台上查看并批准这些更新操作,而无需手动修改相关配置或使用临时标签。
请创建文件.github/dependabot.yml,内容如下:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: ""
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
当被标记为“固定依赖项”的SHA版本有新更新时,Dependabot会自动创建拉取请求。你可以像处理应用程序的依赖项更新一样,对这些请求进行审核并合并它们。
在建立完善的审核流程之前,请不要将Dependabot设置为自动合并这些更新。因为新的更新内容可能包含新的权限设置、修改后的脚本,或是新的依赖关系。
对于每一个更新拉取请求,都需要对比旧版本和新版本的代码差异,查看该操作的发布说明,并检查action.yml文件中JavaScript代码包、shell脚本以及工作流权限设置是否发生了变化。要确保新版本确实属于你打算采用的那个版本。如果该操作涉及部署、发布、管理凭证等高影响操作,请在测试仓库或环境中先运行该工作流进行验证。
我们的目标并不是拒绝所有的更新请求,而是要让决策过程更加透明且可重复执行。审核者在合并更新之前,应该能够回答以下三个问题:哪些代码发生了变化?为什么需要使用新版本?这些新代码会拥有哪些权限?
为特定操作设置允许列表
操作允许列表是一种仓库或组织级别的策略,用于限制哪些发布者或仓库的工作流可以被调用。这样做可以降低贡献者将未知或未经审核的操作引入到受信任的工作流程中的风险。
组织管理员可以在设置 → Actions → 策略 → 允许指定的操作中配置这一规则。请选择与你的工作流程最匹配的策略选项,然后只添加那些已经被团队审核过的仓库。
一些示例允许列表模式如下:
actions/checkout@*
actions/setup-node@*
actions/cache@*
docker/*
my-org/*
按照这种配置,不符合允许列表模式的操作将会被拒绝。属于该组织的所有仓库都会继承这一策略,不过具体实施效果还会受到组织所使用的GitHub计划及仓库设置的影响。
对于没有组织级策略的单个仓库,可以在设置 → Actions → 通用设置 → 允许GitHub创建的操作,并选择非GitHub来源的操作中,仅允许经过验证的创建者来提交这些操作请求。
允许列表用于限制哪些操作身份可以执行特定的操作。它并不能替代SHA校验机制。在任何工作流程中,被允许执行的操作仍然需要通过经过审核的完整SHA值来进行验证。
在拉取请求中强制使用SHA校验
在持续集成环境中运行工作流检查工具,并添加一项仓库检测规则:如果某个工作流程中出现了非SHA格式的引用,该检测规则就会报错。在将某项检查工具用于受保护的工作流程之前,先将其自身的SHA值进行固定。请用您所选的操作检查工具发布的完整提交SHA值替换以下占位符:
name: actionlint
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: rhysd/actionlint@03d0035246f3e81f36aed592ffb4bebf33a03106 # v1.7.7
with:
args: -color
与处理actions/checkout时一样,需要解析版本标签,审核相关提交信息,并记录下那40个字符长的完整SHA值。
您也可以使用简单的检查机制来发现那些常见的错误引用。不过,请将这种检查视为一种辅助手段,而不是依赖它来确保YAML格式的正确性或整个供应链的安全性:
- name: 拒绝使用错误的操作标签
run: |
if grep -RInE '^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*[^#]+@(main|master|v[0-9]+)([[:space:]]|$)' .github/workflows/; then
echo "发现错误的操作标签。请使用完整的SHA值进行校验。"
exit 1
fi
这种检查机制应该同时应用于拉取请求和受保护的分支上。因为即使某个拉取请求通过了这项检查,其中仍可能包含恶意提交,因此还需要结合代码审核、允许列表以及SHA值验证流程来确保安全。
审查第三方操作
在将某个社区提供的操作添加到允许列表之前,请先执行以下步骤:
阅读该操作的
action.yml文件及其入口脚本。查看该操作的星标数量、维护者的声誉以及是否存在未解决的安全问题。
如果该操作非常重要但由外部团队维护,那么请将其SHA值固定下来,并将其分支克隆到
my-org/action-name目录中。
同时还需要检查该工作流程所拥有的权限。那些能够读取仓库机密信息或修改发布内容的操作,显然需要比那些仅仅用于格式化文件的操作受到更严格的审查。因此,请将工作流程的顶级权限设置为最低限度的必要权限,只有在确实需要的情况下,才为具体的任务授予额外的权限。
如何验证这些措施的有效性
对于您批准使用的每一个操作,都要运行SHA值验证脚本,并确保所有的比较结果都是正确的。
创建一个测试拉取请求,将某个操作的标签修改为
@main,然后确认持续集成系统会拒绝这个请求。在错误的操作标签后面添加注释,然后确认您的解析工具或检查工具仍然会拒绝接受这种格式的标签。
验证组织政策是否能够阻止那些被禁止的操作在测试工作流程中被执行。
确认Dependabot是否会自动触发操作更新拉取请求,以及这些变更是否能正常经过审核流程。
检查工作流程的权限设置,确保这些操作无法访问它们并不需要的敏感信息。
当这种方法出现问题的时候
紧急补丁:使用SHA值进行固定会延缓热修复的速度。依赖检测工具加上值班人员的审核,比临时使用临时标签更为安全。
私有仓库中的复合操作:对于私有仓库中的内部操作,也应与公开仓库中的操作一样,使用SHA值进行固定。
可重用的工作流程:不仅要将可重用的工作流程的引用地址用SHA值固定下来,其中包含的各个步骤也都需要进行同样的处理。
带注释的标签与镜像:在记录SHA值之前,需要先将带注释的标签解析为对应的提交信息;同时,还需要通过自身的变更控制流程来验证内部镜像的合法性。
虚假的安全感:
即使使用了SHA值进行固定,但如果不实际阅读代码,仍然会信任在固定时指定的维护者。因此,应该将SHA值固定与允许列表、最小权限设置以及审核机制结合起来使用。
结论
通过本教程,您学会了如何通过将GitHub Actions与经过审核的提交信息中的SHA值关联起来、验证这些引用地址、仅允许特定的操作、启用依赖检测工具的更新功能,以及在拉取请求检查中强制执行这些固定措施,从而防止CI依赖关系出现安全问题。
这样就能形成多层次的安全控制机制:经过审核的不可更改的引用地址、受限制的操作列表、自动化的更新提示,以及能够在合并之前发现回归问题的拉取请求检查机制。
参考资料
相关文章
对于开发者来说,macOS 27带来了哪些新功能?
macOS 27 Golden Gate已经上市了。苹果于2026年9月14日发布了这一版本,所有能够运行它的Mac设备都可以免费升级。 我会向您说明在macOS 27中开发者们会遇到哪些实际变化,如何快速完成升级过程,以及如何确认软件开发所需的工具是否已经安装完毕。 您是否应该进行升级呢?我的建议是:如果您的Mac使用了苹果自家的Silicon芯片,那么**强烈推荐您进行升级**;如果您的Mac使用的是Intel处理器,那么**最好下载相应的安装包**而非通过“软件更新”功能来升级,同时借此机会对您的Mac系统进行一次全面的检查和维护。 如果您已经做好了准备,那么升级本身并不会带来任何问题。
阅读全文
如何编写能够真正被编译成功的Linux内核模块
Linux中的 内核模块 是一段较小的代码,可以在不重新构建整个内核的情况下被加载到正在运行的内核中。 这听起来很简单,但实际上,即使是最简单的模块也会产生大量相关的文件和数据:对象文件、元数据、导出的符号以及未解析的符号,最后还会生成一个与普通可执行文件截然不同的 .ko 文件。 下面是一个完整的、可以正常工作的Linux内核模块。它的代码仅有22行,其中7行是包含头文件和元数据: #include #include #include MODULE LICENSE("GPL"); MODULE AUTHOR("Chris Roy"); MODULE DESCRIPTION("一个最小的可加载
阅读全文
学习如何部署、保护以及自动化全栈Web应用程序的开发过程。
云基础设施 配置Ubuntu服务器,启用安全的SSH密钥认证机制,并完全禁用密码登录和root账户登录。 安全与防火墙设置 使用UFW阻止未经授权的端口访问,同时配置Fail2Ban来自动拦截暴力攻击。 应用运行环境搭建 安装Python和Node.js,为资源有限的服务器配置交换内存,并使用Gunicorn和Supervisor来管理后台进程。 数据存储与搜索功能 自行搭建轻量级的Meilisearch实例,为其分配专用的系统用户账户,并实现数据库快照的自动化生成。 全球部署方案 通过DNS将自定义域名映射到服务器上,使用Let’s Encrypt证书确保连接安全,配置Nginx作为反向代理
阅读全文
随着人工智能技术的发展,跨平台开发所面临的权衡因素发生了变化,因此Shopify放弃了React Native,转而选择使用Swift和Kotlin进行开发。
Shopify最近宣布将放弃使用React Native,转而用Swift和Kotlin重新开发其核心应用程序。由于人工智能模型质量的显著提升,移动业务负责人Mustafa Ali重新评估了Shopify对React Native的依赖程度,他认为,在不同移动平台上维护原生代码库所带来的收益与成本之比,现在已经高于使用抽象层技术所带来的收益与成本之比。 作者:Bruno Couriol
阅读全文