Astral 建構的工具被全球數百萬開發者依賴並信任。

這種信任包含了對我們安全態勢的信心:開發者理應期望我們的工具(以及建構、測試和發布這些工具的流程)是安全的。隨著供應鏈攻擊的興起,例如最近的 Trivy 和 LiteLLM 事件,開發者開始質疑他們是否還能信任他們的工具。

為此,我們希望分享一些我們用於保護工具的技術,希望能對以下對象有所幫助:

我們透過廣泛的 CI/CD 工作流程來維持 Ruff、uv 和 ty 的開發速度,這些工作流程運行在 GitHub Actions 上。沒有這些工作流程,我們將難以以我們要求的速度和信心程度來審查、測試和發布我們的工具。我們的 CI/CD 工作流程也是我們安全態勢的關鍵部分,因為它們使我們能夠將關鍵的開發和發布流程遠離本地開發者機器,置於受控、可觀察的環境中。

GitHub Actions 對我們來說是一個合乎邏輯的選擇,因為它與 GitHub 有緊密的內建整合,並且對貢獻者工作流程有成熟的支持:任何想要貢獻的人都可以使用我們自己使用的相同流程來驗證他們的拉取請求是否正確。

不幸的是,這也有一個反面:GitHub Actions 的預設安全設定很差,像 Ultralytics、tj-actions 和 Nx 這樣的安全漏洞都始於常見的弱點,例如 pwn requests。

以下是我們為保護 CI/CD 流程所做的一些事情:

我們禁止在整個 GitHub 組織中使用 GitHub 中許多最危險且不安全的觸發器,例如 pull_request_target 和 workflow_run。這些觸發器幾乎不可能安全使用,攻擊者不斷找到濫用它們的方法,所以我們根本不允許使用。

根據我們的經驗,許多專案認為它們需要這些觸發器,但絕大多數情況下,最好是將其替換為權限較低的觸發器(例如 pull_request)或完全移除。例如,許多專案使用 pull_request_target,以便由第三方貢獻者觸發的工作流程可以在 PR 上留下評論,但這些用例通常可以透過作業摘要或僅將相關資訊保留在工作流程日誌中來妥善處理。

當然,有些用例確實需要這些觸發器,例如任何確實需要在第三方問題或拉取請求上留下評論的操作。在這些情況下,我們建議完全放棄 GitHub Actions,而是使用一個 GitHub App(或 webhook)來監聽相關事件並在獨立的上下文中執行操作。我們在下面的「自動化」部分更詳細地介紹了這種模式。

我們要求所有動作都固定到特定的提交(而不是標籤或分支,因為它們是可變的)。此外,我們會交叉檢查這些提交,以確保它們與實際發布的儲存庫狀態相符,並且不是偽造的提交。

我們透過兩種方式做到這一點:首先是使用 zizmor 的 unpinned-uses 和 impostor-commit 審核,然後再次使用 GitHub 自己的「要求動作固定到完整的提交 SHA」策略。前者提供了一個我們可以本地運行的快速檢查(並防止偽造提交),而後者是一個對工作流程執行的硬性限制,實際上確保了所有動作,包括巢狀動作,都已完全進行雜湊固定。

啟用後者是一項艱鉅的任務,因為它也要求間接使用的動作(我們調用的動作所調用的動作)進行雜湊固定。為了實現這一點,我們與我們的下游專案(例如)協調,將雜湊固定應用於我們的整個依賴圖。

總之,這些檢查提高了我們對工作流程可重現性和封閉性的信心,進而提高了我們對其安全性的信心(在攻擊者能夠破壞依賴動作的情況下)。

然而,雖然這是必要的,但這還不夠:雜湊固定確保了動作的內容是不可變的,但並不能阻止這些不可變的內容做出可變的決定(例如從 GitHub 儲存庫的發布中安裝最新版本的二進位檔)。目前 GitHub 和第三方工具在偵測這類不可變性差距方面表現都不佳,因此我們目前依賴於手動審查我們的動作依賴項來偵測這類風險。

當手動審查確實發現差距時,我們會與我們的上游專案合作來彌補這些差距。例如,對於內部使用原生二進位檔的動作,這可以透過嵌入二進位檔下載 URL 與加密雜湊之間的映射來實現。這個雜湊反過來成為動作不可變狀態的一部分。雖然這不能確保二進位檔本身是真實的,但它確實確保了攻擊者無法有效篡改指向二進位檔的可變指標(例如非不可變的標籤或發布)。

我們在多個地方限制了我們的 Workflow 和 Job 權限:我們預設在組織層級設定為唯讀權限,並且額外地以 permissions: {} 開始每個 Workflow,然後僅在逐個 Job 的基礎上擴展。

我們盡可能隔離我們的 GitHub Actions 秘密:我們不使用組織或儲存庫層級的秘密,而是使用部署環境和特定於環境的秘密。這使我們能夠進一步限制潛在洩漏的影響範圍,因為一個被破壞的測試或 linting Job 將無法存取,例如,發布發行構件所需的秘密。

為了做到這些,我們利用了 GitHub 自身的設定,以及 zizmor(用於靜態分析)和 pinact(用於自動固定)等工具。

除了我們的 CI/CD 流程之外,我們還採取了多項措施來限制 Astral 組織內帳戶和儲存庫被破壞的可能性和影響:

我們限制了擁有管理員和其他高權限角色的帳戶數量,大多數組織成員僅擁有對他們需要處理的儲存庫的讀取和寫入權限。這減少了攻擊者可以破壞以獲得組織層級控制權的帳戶數量。

我們對 Astral 組織的所有成員強制執行強制的雙重驗證 (2FA) 方法,這超越了 GitHub 預設要求的任何 2FA 方法。實際上,這要求所有 Astral 組織成員都擁有不弱於 TOTP 的 2FA 方法。如果 GitHub 允許我們僅強制執行防網路釣魚的 2FA 方法(例如僅限 WebAuthn 和 Passkeys),我們將這樣做。

我們在整個組織範圍內強制執行分支保護規則:對 main 的變更不能被強制推送,並且必須始終透過拉取請求進行。我們還禁止創建特定的分支模式(例如 advisory-* 和 internal-*),以防止安全工作的過早洩露。

我們強制執行標籤保護規則,這些規則在發行部署成功之前阻止發行標籤的創建,而發行部署本身則需要至少另一名團隊成員的手動批准。我們還阻止更新或刪除標籤,使其一旦創建就變得不可變。在此之上,我們還添加了分支限制:發行部署只能針對 main 創建,防止攻擊者使用不相關的第一方分支來嘗試繞過我們的控制。

最後,我們禁止儲存庫管理員繞過上述所有保護措施。我們所有的保護措施都在組織層級強制執行,這意味著即使攻擊者成功破壞了對特定儲存庫具有管理員存取權的帳戶,他們仍然無法禁用我們的控制。

為了幫助其他人實施這類分支和標籤控制,我們正在分享一個 gist,其中顯示了我們使用的一些規則集。這些規則集特定於我們的 GitHub 組織和儲存庫,但您可以將它們作為您自己政策的起點!

有些事情 GitHub Actions 可以做,但無法安全地做,例如在第三方問題和拉取請求上留下評論。大多數時候,最好是放棄這些功能,但在某些情況下,它們是我們工作流程中有價值的組成部分。

在後者情況下,我們使用 astral-sh-bot 將這些任務安全地隔離在 GitHub Actions 之外:GitHub 向我們發送與 GitHub Actions 接收到的相同的事件數據(因為 GitHub Actions 使用與 GitHub Apps 相同的 webhook payload),但具有更多的控制權和更少的隱含狀態。

然而,GitHub Apps 仍然有一個問題:App 並不能消除操作所需的任何敏感憑證,它只是將它們移到一個不會像 GitHub Actions 那樣廣泛混合程式碼和數據的環境中。例如,App 不會像 Workflow 那樣容易受到模板注入攻擊,但仍可能包含 SQLi、提示注入或其他允許攻擊者濫用 App 憑證的弱點。因此,將 GitHub App 開發視為與任何其他軟體開發一樣的安全性思維至關重要。這也延伸到不受信任的程式碼:使用 GitHub App 並不能使其能夠安全地運行不受信任的程式碼,它只是讓意外運行變得更困難。如果您的流程需要運行不受信任的程式碼,它們必須使用 pull_request 或其他「安全」觸發器,這些觸發器不會向第三方拉取請求提供任何特權憑證。

話雖如此,我們發現 GitHub App 模式對我們來說效果很好,並且我們將它推薦給有類似需求的維護者和專案。它主要的缺點在於複雜性:它需要開發和託管一個 GitHub App,而不是編寫一個由 GitHub 為您協調的 Workflow。我們發現 Gidgethub 等框架使 GitHub App 的開發過程相對簡單,但託管仍然是時間和成本上的負擔。

對於個人和業餘開源專案來說,目前還沒有很好的 GitHub App 選項,這是一個令人遺憾的現實;我們希望這個領域的可用性增強能夠由有資源來彌補 GitHub Actions 作為平台的不足的公司和大型專案來引領。

我們推薦 Mariatta 的這個教學,作為入門 Python 中 GitHub App 開發的一個好方法。我們也計劃在未來開源 astral-sh-bot。

到目前為止,我們已經涵蓋了與 GitHub 緊密相關的方面,因為它是 Astral 工具的來源託管。但我們的許多用戶透過其他機制安裝我們的工具,例如 PyPI、Homebrew 和我們的 Docker 映像檔。這些分發管道為比喻性的供應鏈增加了另一個「環節」,需要單獨考慮:

在可能的情況下,我們使用 Trusted Publishing 發布到註冊中心(例如 PyPI、crates.io 和 NPM)。這項技術消除了對長期註冊中心憑證的需求,從而緩解了最常見的套件接管來源之一(CI/CD 平台中的憑證洩漏)。

在可能的情況下(目前是我們的二進位檔和 Docker 映像檔發布),我們生成基於 Sigstore 的聲明。這些聲明建立了發布構件與產生它的 Workflow 之間的加密可驗證連結,進而允許用戶驗證他們構建的 uv、Ruff 或 ty 是否來自我們的實際發布流程。您可以將我們最近為 uv 製作的聲明作為範例。

我們使用 GitHub 的不可變發布功能來防止事後修改我們在 GitHub 上發布的構建。這解決了一種常見的攻擊者轉移技術,即用惡意構建替換先前發布的構建。最近的 Trivy 攻擊中就使用了這種技術的一個變體,攻擊者透過強制推送覆蓋先前的標籤來引入受感染版本的 trivy-action 和 setup-trivy 動作。

我們不使用快取來提高發布期間的構建時間,以防止攻擊者透過 GitHub Actions 快取中毒攻擊來破壞我們的構建。

我們的發布流程隔離在專用的 GitHub 部署環境中。這意味著不在發布環境中運行的作業(例如測試和 linters)無法存取我們的發布秘密。

為了啟動發布環境,啟動作業必須獲得 Astral 組織中至少另一名特權成員的批准。這減輕了單個惡意或被破壞的帳戶能夠發布惡意版本(或洩漏發布秘密)的風險;攻擊者需要至少破壞兩個不同的帳戶,並且都具有強大的 2FA。

在我們擁有大量發布作業的儲存庫(例如 uv)中,我們使用一個獨立的發布閘道環境來處理 GitHub 為每個使用發布環境的作業觸發批准的事實。這保留了兩人批准的要求,並增加了一個額外的步驟:一個小型、權限最低的 GitHub App 透過部署保護規則來協調從發布閘道到發布的批准。

最後,我們使用標籤保護規則集來防止在發布部署成功之前創建發布的標籤。這可以防止攻擊者繞過正常發布流程直接創建標籤和發布。

我們的發布流程還涉及「連鎖」變更,例如更新我們的公開文檔、版本清單和官方 pre-commit hooks。這些是特權操作,我們透過專用的機器人帳戶和透過這些帳戶發行的細粒度 PAT 來保護它們。

展望未來,我們還在考慮為 macOS 和 Windows 添加帶有官方開發者證書的代碼簽名。

最後但同樣重要的是依賴項的問題。與幾乎所有現代軟體一樣,我們的工具依賴於第三方依賴項生態系統(直接和傳遞性),每個依賴項都處於隱含的信任位置。以下是我們用來衡量和減輕上游風險的一些方法:

我們使用 Dependabot 和 Renovate 等依賴項管理工具來保持我們的依賴項更新,並在我們的依賴項包含已知漏洞時通知我們。

一般來說,我們結合使用上述方法和冷卻期,以避免在新版本發布後立即更新依賴項,因為這時暫時受損的依賴項最有可能影響我們。

Dependabot 和 Renovate 都支持冷卻期,uv 也內建了支援。我們發現 Renovate 能夠按組配置冷卻期特別有用,因為它允許我們放寬對我們自己(第一方)依賴項的冷卻期要求,同時為大多數第三方依賴項保留它。

我們與許多上游依賴項保持社交聯繫,並與它們進行常規和安全貢獻(包括修復它們自己的 CI/CD 和發布流程)。例如,這是我們最近對 apache/opendal-reqsign 的貢獻,以幫助他們降低 CI/CD 安全性。

另外,我們與生態系統中的鄰近專案和工作組保持社交聯繫,包括 Python Packaging Authority 和 Python Security Response Team。這些聯繫對於共享資訊非常寶貴,例如當針對 pip 的報告也影響 uv(反之亦然),或者當 CPython 的安全版本發布需要 python-build-standalone 的版本發布時。

我們在添加新依賴項時持謹慎態度,並在實際可行且對用戶干擾最小的情況下努力消除依賴項。在接下來的發布週期中,我們希望移除一些與支援罕見壓縮方案相關的依賴項,作為我們與 Python 打包標準保持一致的更大努力的一部分。

更廣泛地說,我們對我們的依賴項帶來的東西也持謹慎態度:我們試圖避免引入二進位檔的依賴項,並仔細審查我們依賴項的功能,以禁用我們不需要或不想要的任何功能。

最後,我們在財務上(以我們的 OSS Fund 的形式)為我們依賴的專案或推動整個 OSS 生態系統發展的專案的可持續性做出貢獻。

開源安全是一個艱難的問題,部分原因在於它實際上是許多問題(有些是技術性的,有些是社會性的)偽裝成一個問題。我們已經介紹了許多我們用來解決這個問題的技術,但這篇文章絕不是一個詳盡的列表。它也不是一個靜態列表:攻擊者是安全流程中的動態參與者,防禦措施必然會隨著他們不斷變化的技術而演變。

考慮到這一點,我們想回顧一下上面提到的一些最值得關注的要點:

尊重 CI/CD 的極限:人們極易在 CI/CD 中做所有事情,但有些事情 CI/CD(尤其是 GitHub Actions)就是無法安全地完成。對於這些事情,最好是完全放棄它們,或者使用 GitHub App 或類似工具將它們隔離在 CI/CD 之外。

話雖如此,重要的是不要矯枉過正而完全拋棄 CI/CD:如上所述,CI/CD 是我們安全態勢的關鍵部分,可能也是您的關鍵部分!令人遺憾的是,保護 GitHub Actions 如此困難,但我們認為與完全不使用託管 CI/CD 所帶來的速度和安全風險相比,這是值得的。

特別是,我們強烈建議使用 CI/CD 進行發布流程,而不是依賴本地開發者機器,特別是當這些發布流程可以用防誤用和防洩漏的憑證方案(如 Trusted Publishing)來保護時。

隔離和消除長期憑證:最常見的後續傳播形式是濫用長期憑證。盡可能完全消除這些憑證(例如,使用 Trusted Publishing 或其他基於 OIDC 的身份驗證機制)。

如果無法消除,請將這些憑證隔離到最小可能的範圍:將它們放入具有額外啟用要求的特定部署環境中,並且僅發行具有完成給定任務所需最低權限的憑證。

加強發布流程:如果您在 GitHub 上,請使用部署環境、批准、標籤和分支規則集以及不可變發布,以減少在帳戶被盜或儲存庫被破壞的情況下攻擊者擁有的自由度。

保持對依賴項的意識:保持對依賴項樹整體健康狀況的意識對於理解您自身的風險狀況至關重要。同時使用工具和勤奮來確保您的依賴項安全,並幫助它們確保它們自己的流程和依賴項也安全。

最後,我們仍在評估上述許多技術,並且隨著我們對它們的局限性以及它們如何與我們的開發流程互動的了解越來越多,我們幾乎肯定會在未來幾週和幾個月內調整(並加強)它們。也就是說,這篇文章代表了一個時間點,而不是我們對開源工具安全性的最終看法。

PyPI 允許上傳帶有聲明的檔案,根據 PEP 740。然而,我們目前沒有將我們的聲明上傳到 PyPI,因為 PyPI 對 Trusted Publishing 的實現與我們用於聲明的身份之間存在一些不兼容性。我們希望在不久的將來解決這些不兼容性。↩

值得注意的是,安裝程式與發布本身來自同一主機,因此直接使用 curl ... | bash 的用戶無法從安裝程式本身的校驗和中獲得實質性好處。然而,安裝程式腳本中的校驗和對希望將我們的安裝程式腳本進行本地化(例如,整合到他們自己的構建或 CI/CD 流程中)的用戶是有益的。↩