2021 年 1 月,攻擊者在一支熱門的 bash 指令稿中加入一行程式碼。數萬家組織下載了該指令稿,並將它納入日常的持續整合流程中執行。每次執行、每次提交、每個管線中,該指令稿都會忠實地把 CI runner 中的所有環境變數傳送到一個陌生的 IP 位址。這種情況持續了 61 天才有人察覺。

此後的幾年,這個事件並非孤例。2024 年的 XZ Utils 後門事件是由一位耐心的維護者花費約兩年時間培養信任,最終將遠端程式碼路徑植入幾乎每個 Linux 發行版都會包含的壓縮函式庫中,這起事件是因為一位 Microsoft 工程師注意到 SSH 登入速度變慢了半秒才意外發現。 

數個月後的 polyfill.io 接管事件,則將嵌入數十萬個網站的指令碼在網域易手後的數天內轉變為惡意軟體傳遞網路。npm 與 PyPI 登錄檔則持續出現遭入侵的套件,數量多到已無人計數。Codecov 已不再是警示故事,而是成了模板。

「Codecov 已不再是警示故事,它就是模板。」

這個故事有趣之處不在於一家名叫 Codecov 的公司在 2021 年遭遇了幾個月的不順,而是讓攻擊得以發生的安全模型,這並非單一廠商獨有的異常,而是現代軟體建置方式的結構性特徵。

核心論點很簡單:管線就是邊界。護城河不在生產環境之前,而是在部署生產環境的東西周圍。

邊界問題

二十年來,安全架構將生產環境視為城堡,而城堡外的一切則被視為荒地。防火牆、WAF、網路分割與 IAM 政策大多指向外部。它們的可信度大約與你能把它們扔多遠一樣,由於它們是無形的,所以可信度並不高。

問題在於,現代生產環境並非城堡,而是一種建構。程式碼提交變成成品、成品變成部署、部署變成可存取客戶資料庫的執行中基礎架構。在這條鏈路的每個環節,自動化系統都持有具備權限的憑證,若不仔細檢視,這些權限實際上等同於「可做任何想做的事」。

你的 CI runner 持有 AWS 金鑰。你的建置代理程式擁有容器登錄檔憑證。你的部署步驟可以與 Kubernetes 溝通。實習生的 GitHub Action 原則上可以推送至 main。若其中任何一項遭到入侵,「生產環境」的區別就會變得只是語義上的。

「每個新團隊、工具與整合,都會擴大能讀取你機密的範圍,而管線幾乎從未獲得已部署基礎架構所擁有的治理。」

Codecov 事件是典型案例,但並非例外。這只是管線權限工具遭到入侵時會發生的事。只要有足夠的工具與時間,入侵就會成為統計上的必然。規模是沒有人預算的變數。每個新團隊、工具與整合,都會擴大能讀取你機密的範圍,而管線幾乎從未獲得已部署基礎架構所擁有的治理。

事件經過

Codecov 製作了一支名為 Bash Uploader 的小型 bash 指令稿。它會從 CI 執行中收集程式碼覆蓋率報告,並將報告傳回 Codecov 進行處理。要使用它,你只需在 CI 設定中加入一行指令:使用 curl 取得指令稿,再將其直接傳送至 bash 執行。

該指令稿託管於 Google Cloud Storage 儲存貯體。Codecov 也建置並發布其自架產品的 Docker 映像檔,而在其中一張公開映像檔的中間層中,Codecov 內嵌了可控制該儲存貯體的 Google Cloud Storage 服務帳戶的 HMAC 金鑰。這類錯誤通常發生在忙碌的人身上。

2021 年 1 月 31 日,攻擊者從 Docker 層中提取該金鑰,利用它直接修改儲存貯體中的 Bash Uploader,並開始向每位下載該指令稿的客戶提供修改後的版本。修改內容僅為單行程式碼,位於約 1,800 行指令稿的第 525 行左右,恰好是大多數人 停止閱讀自稱用於處理無聊程式碼覆蓋率工作的程式碼區域。該行程式碼執行 env 指令,列印目前 shell 中的所有環境變數,並將輸出 POST 至攻擊者控制的伺服器。

CI runner 作為例行作業,包含組織擁有的所有敏感資訊:AWS 存取金鑰、GitHub 權杖、部署金鑰、資料庫憑證、Slack webhook,以及部署管線整合的每個 SaaS 的 API 金鑰。修改後的指令稿忠實地在兩個月內、數萬名客戶的每次執行中,竊取了所有這些資訊。

這起入侵於 2021 年 4 月 1 日被發現。發現者既非 Codecov,也非入侵偵測系統,而是一位客戶恰巧對照 Codecov 公布的 SHA256 對下載的指令稿進行檢查,發現兩者不符,於是寄信詢問 Codecov 是否為故意之舉。

已確認的下游受害者包括 HashiCorp、Twilio、Rapid7 與 Confluent。實際影響範圍無法得知,因為一旦憑證被竊取並用於複製私人存放庫,追蹤就會進入其他人的環境,進而取得更多機密。

Image of containers stacked on top of each other (Tim G for Unsplash).

此事件暴露的結構性問題

人們可能會將 Codecov 事件歸類為「Docker 映像檔衛生錯誤」而不再追究。但這是錯誤的做法。Docker 層問題是攻擊者入侵的「方式」,而不是攻擊造成嚴重後果的「原因」。

攻擊之所以造成嚴重後果,是因為幾乎地球上每支 CI/CD 管線都預設了一個假設:任何同意在管線中執行的工具,都可以被信任持有它所擁有的所有機密。

這個假設並非不合理,只是未被強制執行。典型的 CI runner 並無有意義的「此步驟可存取機密 A 但不可存取機密 B」,或「此步驟可呼叫 AWS 但不可呼叫其他國家隨機 IP」的概念。大多數管線會將完整憑證組合交給每個執行的步驟,理由聽起來合理:這些步驟是由公司內部人員設定的。

「攻擊之所以造成嚴重後果,是因為幾乎地球上每支 CI/CD 管線都預設了一個假設:任何同意在管線中執行的工具,都可以被信任持有它所擁有的所有機密。」

任何你用 curl | bash 帶入 runner 的工具,都會繼承你管線的權限。任何以標籤參照的 GitHub Action 也是如此。你拉取的任何容器映像檔也是如此。任何在先前版本的相依性中出現、當時沒有人特別注意的輔助指令稿也是如此。能讀取你環境變數的項目清單,就是在你管線中執行的項目清單,這個清單比大多數安全檢視所承認的長得多,而且每季都在增加。

這也是為什麼供應鏈攻擊自 2021 年以來並未減緩。Codecov 的故事以各種變化形式與輪替的角色,每隔幾個月就在不同生態系中重演。攻擊類型是結構性的,修復也必須是結構性的。

修復方案的樣貌

若能同時採取三項治理控制措施,本來可以讓 Codecov 入侵事件變成註腳。這些措施並不新奇,但都需要將管線視為生產基礎架構,而非開發者便利工具。

驗證實際執行的內容。最終發現攻擊的客戶是透過比對 SHA256 總和檢查碼做到的。Codecov 當時已公布雜湊值,但幾乎沒有人檢查。管線在執行任意擷取的程式碼(指令稿、容器、Terraform 提供者、GitHub Action)前,應先驗證簽章或總和檢查碼是否符合已知的受信任來源。將 Action 固定到 commit SHA 而非標籤、驗證提供者簽章,以及在任何部署觸及雲端帳戶前對 IaC 執行靜態掃描,這些已不再是專門做法。Checkov、TFsec、Trivy 與 TFLint 等掃描器可以在管線中作為步驟執行,於任何狀態被觸及前完成掃描。這些是基本要求。將它們視為選用項的代價,就是付出兩個月的資料外洩窗口。

限制管線可通訊的對象。Bash Uploader 是透過 CI runner 內部向任意網際網路主機發出 outbound HTTPS 要求來外洩資料。大多數 CI runner 能這麼做,是因為沒有人告訴它們不能這麼做。自我託管或受控的 runner,若在私有網路內執行並將 egress 限制為已知的允許清單,就能將「POST env 到隨機伺服器」變成失敗的網路呼叫。一個只能連線至 VCS、雲端 API 與已宣告的外部相依性的 runner,無論在其指令稿中悄悄插入什麼,都無法被轉用為通用的憑證外洩工具。

停止將靜態機密交給管線。這是影響最重大的變更,也是產業最慢接受的變更。env 指令對攻擊者如此有效的原因,是大多數 CI 環境都充斥著長期有效的靜態憑證:可使用數年的 AWS 存取金鑰、直到有人想起要輪換前都有效的 GitHub PAT。聯合身分識別徹底改變了問題的性質。透過 OIDC,管線不需要儲存任何憑證。它會呈現一份已簽署的聲明,描述它所處的環境、正在執行的 workflow,以及正在部署的 commit,雲端提供者會將該聲明交換為短期的權杖,權杖的範圍僅限於即將執行的操作。被竊取的聲明在數分鐘後就失效。被竊取的權杖在一小時內失效,且無法在聲明所描述的條件之外使用。

一個透過 OIDC 向 AWS、Azure、GCP 與 Vault 進行驗證、在具有 egress 允許清單的網路區段內執行,並驗證其相依的成品與提供者的管線,會讓 Codecov 情境產生的惡意指令稿幾乎無法達成任何目的。env 輸出不含長期有效的機密。外洩目的地無法連線。修改後的指令稿在執行前就會驗證失敗。

這些控制措施無法防止初始入侵,但能讓初始入侵變得無足輕重,這是次佳選擇,在大多數情況下也是可實現的目標。

Modern building exterior with colorful pipes and metal framework. (@heytowner for Unsplash)

將管線視為生產環境

還有一項變更,它更偏向文化而非技術。管線需要獲得生產環境已有的相同治理。這意味著每次部署都要有稽核日誌、針對觸及敏感範圍的變更要有核准政策(最好是 policy-as-code),以及 基於角色的存取控制,讓開發人員不會無意中給予管線超出其自身角色許可的權限。這也意味著 漂移偵測,讓程式碼所述與實際執行內容之間的差距不再是部落知識,而是可追蹤、可警示的事實。

「這些控制措施無法防止初始入侵,但能讓初始入侵變得無足輕重。」

這件事並不光鮮亮麗,但卻是 CI/CD 系統成為攻擊面還是成為執行點的差別。

將這四項控制措施並列,它們描述的是一個單一概念。管線應享有與其所部署基礎架構相同的 持續治理:不是一次性安全檢視,而是在變更執行前、執行期間與執行後都持續施行的控制。驗證執行的內容、限制可通訊的對象、移除靜態憑證,以及透過稽核軌跡與可執行的政策治理每次部署,這些不是附加在平台上的安全功能,而是對長期被視為便利工具的堆疊部分施加治理,並且無論團隊執行十支還是上萬支管線,都以相同方式施加。

你的管線是你邊界的一層,而目前它是你最弱的一層。在它存在的大多數時間裡,它一直被視為生產力工具,而非安全邊界。Codecov 的攻擊者在 2021 年就理解這一點。如今,下一批攻擊者正掃描公開 Docker 映像檔尋找過多的層數,速度比五年前更快,他們也理解這一點。如果我們沒有同樣理解,我們將持續付出越來越高的代價。

YOUTUBE.COM/THENEWSTACK

科技發展迅速,別錯過任何一集。訂閱我們的 YouTube 頻道,即可觀看所有 podcast、訪談、示範與更多內容。

Group Created with Sketch.