iT邦幫忙

2026 iThome 鐵人賽

DAY 28
1
Security

槍林彈雨下的資安防守:從品質觀念切入,帶開發者從零動手作資安 30 天系列 第 28

【Day 28】【動手做】打造自動化安全防線:在 GitHub Actions CI/CD 整合 SAST 自動掃描

  • 分享至 

  • xImage
  •  

💡 今日學習目標:學會在 GitHub Actions 部署自動化 CI/CD 流水線,讓每一次程式碼提交都自動執行 SAST 資安掃描,理解為什麼「掃描」這件事不能只靠工程師手動記得做。


📌 前言:3,900 萬筆外洩機密,不是因為沒人在看

GitHub 在 2025 年初公布了一個值得每個工程師停下來看一眼的數字:光是 2024 年一年,GitHub 的機密掃描(Secret Scanning)就偵測到超過 3,900 萬筆外洩機密(API 金鑰、密碼、Token)被推送進儲存庫。

📖 資料來源:GitHub Blog —《GitHub found 39M secret leaks in 2024. Here's what we're doing to help》

3,900 萬筆是什麼概念?一年有 525,600 分鐘,把它攤平下去是每分鐘 74 次,換句話說:平均每一秒鐘,就有超過一次的機密被推進了某個儲存庫。

你不可能雇用一支團隊,24 小時盯著每一次 git push,用肉眼去找哪一行不小心把 AWS_SECRET_KEY 寫死進程式碼裡。

Day 08,我們在自己的 IDE 裡裝上了 SonarQube for IDE,讓漏洞在打字的當下就被標記出來。但 IDE 級別的掃描有一個天生的限制:它只在「有裝、有開、有看」的那台電腦上生效。 只要有一個人忘記裝、關掉了提示,或是直接用網頁編輯器改了一行程式碼就送出 PR,IDE 那道防線就形同虛設。

今天,我們要把這道防線焊進沒有人能繞過去的地方:CI/CD 流水線本身。


🛠️ Step-by-Step:在 GitHub Actions 配置自動化掃描

⚠️ 步驟 0:先解決一個問題,不然你一定會卡住

你 Day 09 架的那台 SonarQube,GitHub Actions 連不到。

原因很單純:Actions 的執行器(Runner)跑在 GitHub 的雲端機房,而你那台在 http://localhost:9000,那個 localhost 指的是你自己的電腦。雲端的 Runner 連不到它,也永遠連不到。

如果你直接把 SONAR_HOST_URL 填成 http://localhost:9000,工作流程會照跑、然後卡住、最後逾時失敗,畫面上只會看到連線被拒絕。錯誤訊息不會告訴你「因為那是你家的電腦」。

三條路可以走:

做法 適合誰 代價
① SonarQube Cloud(本文採用) 練習、公開儲存庫 公開儲存庫完全免費,不用架任何東西
② 把自架站對外開放 公司內已有正式伺服器 需要固定 IP/網域與 HTTPS
③ 改用自架 Runner 內網環境、不想對外開 Runner 跑在你自己的機器上,就連得到 localhost

本文走第 ① 條,因為它維持了這 30 天「一毛錢都不用花」的前提:到 sonarcloud.io 用 GitHub 帳號登入 ➔ 匯入你的公開儲存庫 ➔ 產生一組 Token,五分鐘就好。

🛑 如果你想選 ②,請先想清楚一件事。 那台機器上有你所有專案的原始碼分析結果,而 Day 09 我們只改過 admin 密碼,沒有做任何其他加固。把它直接開到公網,本身就是一個 Day 19 講的「安全設定缺陷」。 至少要加上 HTTPS、限制來源 IP,或者放在 VPN 後面再說。


步驟 1:在 GitHub Repository 設定機密金鑰(Secrets)

  1. 開啟你的 GitHub 專案倉庫。
  2. 前往 Settings → 選擇 Secrets and variables → 點擊 Actions
  3. 點擊 New repository secret,新增 SONAR_TOKEN,值就是剛剛在 SonarQube Cloud 產生的那組 Token。
  4. 只有走第 ② 條路(自架且對外可達)的人,才需要再新增一個 SONAR_HOST_URL 指向你伺服器的網址。用 SonarQube Cloud 的話不必設,Action 會自己找到正確位置。

🔑 注意這裡用的是 Secrets,不是直接寫進 YAML。 Secrets 的值出現在 Actions 日誌時會被自動遮成 ***,而寫進 YAML 的東西會跟著程式碼進版本庫,任何看得到這個倉庫的人都看得到。這就是 Day 08 那條「硬編碼機密」規則在 CI 環境的版本。

步驟 2:新增 GitHub Actions 工作流定義檔(YAML)

在專案中建立 .github/workflows/security-scan.yml

name: Security & SAST Analysis

# 觸發條件:Push 至 main 分支,或有人對 main 開 Pull Request 時自動觸發
on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonar_scan:
    name: Run SonarQube Security Scan
    runs-on: ubuntu-latest

    steps:
      # 1. 拉取專案程式碼
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 抓取完整 Commit 歷史以精準分析增量修改

      # 2. 呼叫官方 SonarQube Action 進行掃描
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v8
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          # 用 SonarQube Cloud 的話,這一行不用設
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

      # 3. 檢查 Quality Gate 結果,沒 Pass 就讓 CI Build 失敗
      - name: SonarQube Quality Gate Check
        uses: SonarSource/sonarqube-quality-gate-action@v1
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

存檔並推送這支 YAML,下一次你對 main 開 Pull Request 時,GitHub Actions 就會自動觸發整套掃描,不需要任何人手動下指令。

📌 用 SonarQube Cloud 的話,還缺一個東西:專案根目錄要有一支 sonar-project.properties,至少寫上這兩行,否則掃描會直接報錯說找不到 organization:

sonar.organization=你的組織代號
sonar.projectKey=你的專案代號

好消息是你不必自己寫:在 SonarQube Cloud 匯入儲存庫時,精靈會把這兩個值連同一份 workflow 範本一起產生給你,照它給的貼上就好。自架版則不需要 organization 這一行。

⚠️ 請特別注意那兩個 @v8@v1,不要照抄成 @master

網路上大量範例寫的是 @master,意思是「每次執行都抓那個分支的最新程式碼」。這代表別人推一次 commit,你的 CI 下一次跑的東西就變了,而你不會收到任何通知。而 Action 是在你的環境裡執行、拿得到你所有 Secrets 的程式碼。

這不是理論上的擔心。2025 年 3 月 14 到 15 日,熱門的 tj-actions/changed-files 被入侵(CVE-2025-30066):攻擊者把 v1 一路到 v45.0.7 的所有版本標籤,全部改指到同一個惡意 commit,那段程式碼會去掃描 Runner 的記憶體、把找到的憑證印進建置日誌裡。超過 23,000 個儲存庫受影響,美國 CISA 也發布了專門的通報。

請注意這件事最刺的地方:連釘了版本號的人都中招了,因為標籤是可以被移動的。當時唯一沒事的,是把 Action 釘在 commit SHA 上的人。

所以更保險的寫法長這樣(那串 40 位英數字就是 commit SHA):

uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1

後面用註解記下它對應哪個版本,之後升級時才知道自己原本釘在哪。(這串 SHA 請自己去查當下的最新版,不要照抄本文的:在該 Action 的 GitHub 頁面點進 Releases,或直接看 tag 指向哪個 commit。)明天 Day 29 談供應鏈攻擊時,你會發現這整件事就是 SolarWinds 的縮小版:被動手腳的不是你寫的程式碼,而是你信任的那個東西。

步驟 3:在 Branch Protection 開啟「必要狀態檢查」

光是掃描會跑,還不夠。如果沒有這一步,security-scan.yml 失敗時 PR 上只會顯示一個紅色叉叉當參考,Merge 按鈕依然按得下去。真正讓按鈕被物理鎖住的,是下面這個常被新手漏掉的設定:

  1. 前往 SettingsBranches → 對 main 新增或編輯 Branch protection rule
  2. 勾選 Require status checks to pass before merging
  3. 在清單裡把剛剛那支 Run SonarQube Security Scan 加進「必要檢查」。

少了這一步,你辛苦寫的 YAML 只是一個會發出警告聲的旁觀者,不是真正的門禁,這正是「有掃描」跟「有門禁」之間,最容易被忽略的一道牆。


🔍 自動化效果:PR 上的安全門禁長什麼樣子

設定好 Branch Protection 之後,當有團隊成員提交程式碼並發起 Pull Request:

  1. GitHub Actions 自動觸發 security-scan.yml
  2. 掃描工具在雲端分析程式碼,檢查是否含有新的 Security Hotspots 或 Vulnerabilities。
  3. 通過 → 顯示綠色勾勾,允許合併。
  4. 沒通過 → 顯示紅色叉叉,Merge 按鈕直接被鎖住,不是提醒你「建議修復」,是物理上按不下去。

https://ithelp.ithome.com.tw/upload/images/20260916/20007542RzuaWfaz1K.png

(上圖畫面為示意呈現,實際介面依 GitHub 版本而定;但 Quality Gate 鎖住 Merge 按鈕的判定邏輯,完全依照本文 YAML 設定真實運作。)


⚠️ 迷思破解:「我很小心,不會不小心把密碼寫進去」

幾乎每個資深工程師都覺得自己不會犯這種低級錯誤。但 3,900 萬筆這個數字裡,絕大多數不是新手犯的錯:是趕時間時的臨時測試程式碼、是忘記加進 .gitignore 的設定檔、是複製貼上範例程式碼時忘記換成環境變數

人不可能保持 100% 的注意力,但自動化可以。這正是 Day 08 到今天真正想傳達的邏輯遞進:IDE 級掃描負責「早期提醒」,CI 級掃描負責「最後把關」,前者靠自覺,後者不需要靠任何人自覺,因為它根本不給你選擇要不要遵守。


🎯 今日重點小結與防守心法

  • 🔹 心法 1:自動化取代人工記憶。把資安檢測寫成 CI/CD Workflow,不留給任何人「忘記」的空間。
  • 🔹 心法 2:PR 門禁化(Gatekeeper)。Quality Gate 沒過,程式碼就物理上進不了主幹。
  • 🔹 心法 3:IDE 負責提醒,CI 負責把關。前者靠自覺,後者不需要任何人自覺。兩層疊起來,才擋得住那個「每秒鐘超過一次」的外洩速率。

💬 明日預告:【Day 29】DevSecOps 落地心法:如何在開發效率與資安防守之間取得平衡?
明天我們要探討的是:連 CI 都掃過、連建置流程都通過了,2020 年的 SolarWinds 事件依然告訴我們,供應鏈安全還有一塊自動化掃描碰不到的死角。


上一篇
【Day 27】資安是管理出來的:ISO 27001 / ISMS 到底在管什麼?新手也能懂的管理精髓
下一篇
【Day 29】DevSecOps 落地心法:如何在開發效率與資安防守之間取得平衡?
系列文
槍林彈雨下的資安防守:從品質觀念切入,帶開發者從零動手作資安 30 天30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
AndyAWD
iT邦研究生 5 級 ‧ 2026-09-16 23:31:28

果然缺金鑰去 GitHub 找就對了

我要留言

立即登入留言