為提交和部署程式碼的 CI 機器人落實最小權限原則
擁有廣泛寫入權限和無限制部署權限的自動化機器人,會將一次錯誤的執行變成整個系統的故障。以下說明如何縮小其爆炸半徑。
一個會提交程式碼、推送分支和觸發部署的自動化機器人,是一種具有生產環境存取權限且無人為介入的機器帳號。簡易的設定賦予其長期有效的權杖 (token)、整個程式碼倉庫的寫入權限,以及部署任何東西的權限。這種方式第一次就能成功,所以它會擴散開來。這也意味著一次單一的失誤、一次提示詞注入 (prompt injection),或一次權杖洩漏,就能夠在任何人注意到之前,重寫任何檔案並發布任何服務。這篇文章是關於縮小那個爆炸半徑:給予每個機器人自己的身分、短期有效的憑證、一個狹窄的路徑允許清單,以及簽署過的輸出,如此一來,一次錯誤的執行只會損壞一個角落,而不是全部。
為何機器人的爆炸半徑比人類的更大
一個擁有廣泛存取權限的人類,仍然會受到人類速度的限制。他們會打開幾個檔案、思考、進行變更,然後由同事審核拉取請求。錯誤往往規模小且速度慢,足以被發現。
機器人則沒有這些煞車機制。它以毫秒為單位行動,在其能觸及的每個儲存庫中重複相同的動作,並且不會停下來思考某個變更是否看起來有問題。如果其邏輯有缺陷或帳號被接管,那麼當初為了方便設定而授予的廣泛權限,現在反而讓攻擊者能以機器速度在權杖的完整範圍內移動。方便的預設值與最糟情況的事件,其實是同一組權限在順利與不順利時的不同面貌。
因此,設計目標並非「信任機器人」。機器人無法像人類那樣被信任,因為其背後沒有判斷力,只有規則以及被餵入的任何輸入。目標是限制機器人能做的事情,如此一來,即使是一次完全被入侵的執行,其影響範圍也能維持在很小的規模。應將每個自動化帳號視為一種有限的能力,而非一個受信任的行動者。
為每個機器人提供其專屬身分
第一個錯誤是在多個作業之間共用一個強大的帳號。一個單一的機器身分,讓測試執行器、發布作業、文件發布器以及三個自動化腳本都用它來進行驗證,這會是一個完全失敗的單點。這些作業中只要有任何一個遭到入侵,就會交出該共用帳號所能做的一切權限的聯集。
將它們分開。每個不同的作業都應有其專屬的身分,且只具備該作業所需的權限。用來發布文件的身分不應該能夠部署支付服務。用來執行測試的身分應該只有讀取權限,僅此而已,因為測試不需要對生產環境進行任何寫入操作。
獨立的身分也讓稽核日誌更具可讀性。當一個帳號包辦所有事情時,一條日誌寫著「CI 帳號推送了一個 commit」幾乎無法告訴你任何資訊。當文件機器人有自己的名稱時,一筆顯示文件機器人正在寫入文件目錄之外的紀錄,就是一個你可以發出警報的明顯異常。依據個別身分劃定範圍,能將你的存取日誌轉變為一個有效的入侵信號。
將權限範圍限定於精確的路徑和資源
最小權限原則意味著授予的權限符合工作所需,而非平台。一個更新變更日誌的機器人需要對一個檔案的寫入權限,而非整個樹狀結構。一個部署單一服務的機器人需要對該服務的部署權限,而非專案中的所有服務。
在此,兩種限制完成了大部分的工作。限制機器人可以操作的資源,並限制它可以寫入的路徑。部署身分應綁定至特定的服務或命名空間,如此一來,即使是有效的權杖也無法動到任何其他東西。一個提交機器人應被限制在一個目錄中,這可以透過分支保護規則來拒絕其範圍外的變更,或是透過一個檢查,在差異偏離時讓執行失敗。
# Per-bot permission model. Each identity gets only what its job requires.
identities:
changelog-bot:
repo_write: true
path_allowlist:
- "CHANGELOG.md"
- "docs/releases/**"
deploy: none
service-deploy-bot:
repo_write: false # deploys, does not commit
deploy:
target: "web-frontend" # this one service, nothing else
environments: ["staging", "production"]
test-bot:
repo_write: false # read-only; tests never write prod
deploy: none
將模型明確寫出來的重點在於,你可以更精確地做出「賦予其寫入權限」這個決定。幾乎每個機器人都需要遠少於完整的授權,而且一旦設定好,較窄的授權在執行期間不會產生任何成本。
廣泛存取與最小權限的並列比較
這兩種設定在平時看起來很相似。但在出狀況時則完全不同。下表呈現的是在兩種模型下,同一個自動化機器人的情況。
| 屬性 | 廣泛存取 | 最小權限 |
|---|---|---|
| 身分 | 所有任務共用一個帳號 | 每個任務一個身分 |
| 儲存庫寫入 | 整個樹狀結構 | 僅限列入允許清單的路徑 |
| 部署範圍 | 任何服務、任何環境 | 一個指定的服務與環境 |
| 憑證生命週期 | 長期有效的權杖,無到期日 | 每次執行時鑄造,數分鐘後到期 |
| 外洩的權杖會洩漏什麼 | 完整的寫入與部署權限,無限期 | 一個路徑或一個服務,直到權杖到期為止 |
| 一次錯誤執行的影響範圍 | 任何檔案、任何服務 | 僅限該機器人的通道 |
| 稽核訊號 | 「ci 帳號做了某件事」 | 指名的執行者、超出通道的行為警報 |
| 輪替負擔 | 手動,容易忘記 | 無,因為憑證是短暫的 |
最重要的幾列是關於外洩和錯誤執行的那幾列。在廣泛存取模型下,一個被洩漏的權杖或一次有錯誤的執行,其影響會觸及所有東西。在最小權限模型下,同樣的失敗會被限制在單一路徑或單一服務中,而且短暫的憑證生命週期意味著一旦執行結束,被竊取的權杖就幾乎毫無價值。
使用短期憑證汰除長期有效的密鑰
儲存在密鑰中的長期有效權杖是一個持續存在的風險。它不會過期,可能會透過日誌或受損的依賴項外洩,而且如果真的外洩,你通常不會知道,因為被竊取的權杖所產生的呼叫,看起來與真實機器人的呼叫完全相同。輪替是件繁瑣的手動工作,所以權杖會不斷累積,並且存活時間超過需要它們的任務。
更好的模式是為每次執行新鑄造憑證,並讓其在幾分鐘後過期。許多自動化平台可以為一次執行提供一個已簽署的身份斷言,而目標系統可以被設定為信任某個特定呼叫者的該斷言,並將其交換為一個短期存取權杖。沒有儲存持久性的密鑰,所以沒有東西可以外洩、輪替,或在幾個月後被發現在密碼庫中遺忘。這種信任被鎖定在確切的身份上,而且通常是確切的分支,所以來自任何其他地方的斷言在接觸到實際權限之前,就會在交換時失敗。
當平台無法為每次執行鑄造憑證,而你只能使用儲存的權杖時,請將其設為範圍受限且短期輪替的權杖,並將其範圍保持得與上述的最小權限模型一樣狹窄。一個範圍嚴格受限的短期金鑰是一個可行的備用方案。而一個範圍廣泛、永不過期的金鑰,則是應該在設計中汰除的東西。
將內容寫入與部署分離,避免單一洩漏造成連鎖反應
一個微妙的陷阱是,為了方便而將所有權限捆綁到單一憑證中。如果同一個權杖既可以提交程式碼又可以觸發部署,那麼洩漏它就等於交出了整個鏈條:攻擊者可以一步到位地寫入惡意變更並將其發布,沒有第二道屏障。
按功能分離權杖。用於寫入內容的憑證不應具有部署能力,而用於部署的憑證也不應具有寫入內容的能力。現在,寫入端的洩漏可能會污染檔案,但無法自行將其上線;而部署端的洩漏可以重新部署現有的產物,但無法引入新的惡意程式碼。攻擊者需要同時取得兩者才能完成端到端的入侵,這比只需要一個的門檻要高得多。
# Two credentials, two jobs, no overlap.
content-writer:
can_commit: true
can_deploy: false # writing alone cannot reach production
deployer:
can_commit: false # deploying alone cannot introduce new code
can_deploy: true
target: "web-frontend"
這與將讀取和寫入分開的道理相同,只是應用在更高一個層級上。將職責分散到不同的憑證上,意味著沒有任何單一被盜的密鑰,能一路從原始碼變更執行到線上流量。
簽署 commit 和產出物以便您能驗證來源
界定範圍限制了機器人能做什麼。簽署讓您能證明機器人實際做了什麼。當機器人簽署其 commit 和它所建置的產出物時,每一份輸出都帶有可驗證的來源標記,任何未經簽署或由錯誤金鑰簽署的東西都明顯是外來的。
這很重要,因為一個狹窄的權限集仍然留有空間,讓一個混淆或被劫持的機器人在其範圍內產生輸出。簽章將該輸出轉變為您可以檢查的東西。部署步驟可以拒絕交付未經預期建置身分簽署的產出物,因此,將未簽署的二進位檔偷偷放入管線的攻擊者會在門口被拒絕,而不是被交付。一個已簽署的 commit 歷史記錄意味著,一個偽造的、聲稱來自發布機器人的 commit 會驗證失敗,因為攻擊者沒有該機器人的簽署金鑰。
當機器人的輸出會流入後續執行的某個東西時,簽署就值得進行設定。將其與上述的短期憑證結合,保持簽署金鑰本身的最小化和獨立性,來源驗證就會變成一個自動檢查,而不是手動的信任決策。
權衡取捨,以及何時應保持簡單
這一切都不是免費的。坦白說,成本在於設定的複雜性與持續的架構。一個帶有廣泛權限權杖的共享帳戶,只需點擊幾下即可完成。最小權限的版本則需要多個身分、每個提交機器人對應的路徑允許清單、每個部署機器人對應的資源綁定、用於短期憑證的聯合信任、拆分的內容與部署權杖,以及帶有自身驗證步驟的簽署金鑰。這需要確實的工夫,特別是每次執行的憑證信任,很容易在不經意間設定得過於寬鬆,導致它看似運作正常,但實際上信任了比您預期更多的呼叫者。請編列時間來精確地撰寫這些信任條件,並測試來自錯誤管道的身分是否確實會被拒絕。
回報是,這份成本只需支付一次,之後便大致消失,而它所消除的風險卻是持續且反覆發生的。沒有需要輪替的權杖,沒有範圍會悄悄擴大的共享帳戶,而且一次被入侵的執行會被限制在單一路徑或單一服務中,而不是影響整個系統。
判斷力依然適用。一個沒有真實資料、生命週期只有幾天的拋棄式沙箱,並不需要全套的機制,因為當整個專案下週就要被刪除時,其爆炸半徑已經很小了。一個針對模擬器運行的本地腳本,完全不需要任何生產環境憑證。當一個機器人擁有對重要事物的常駐存取權限時,這套完整模型就值回成本了:例如人們所依賴的儲存庫、通往真實使用者的部署路徑、或是在生產環境中運行的成品。對於任何會自行對線上系統進行提交或部署的自動化作業,廣泛的授權為今日省下了幾分鐘,卻換來了在其存在期間內整個系統的責任風險。最小權限的代價是一次性的、一個較長的下午,但之後能讓每次出錯的執行所造成的影響都保持在小範圍內。