從 Day 24 到 Day 28,我們陸續談了 SAST、依賴掃描、容器掃描、DAST、Secrets 管理。
今天要把這些安全檢查收斂起來,組合成一條真正的 安全 CI/CD 流程 (Secure Pipeline)。
這樣的設計不只是附加檢查,而是把安全視為軟體交付的基本要件。
👉 這張圖示意了安全檢查如何融入每個階段:
🔹SAST:提交程式碼時檢查潛在漏洞。
🔹依賴掃描:Build 階段檢查套件是否有已知漏洞。
🔹Trivy:容器 Image 打包後做掃描,避免帶著漏洞上線。
🔹DAST:部署到測試環境後模擬攻擊。
🔹Sealed Secrets:敏感資訊全程安全管理。
以 GitHub Actions 為例,這裡示範如何把幾個常見安全檢查工具串進 CI:
container_scan:
runs-on: ubuntu-latest
needs: deps
steps:
- uses: actions/checkout@v4
- name: Build Docker image (tag = commit sha)
run: |
docker build -t myapp:${{ github.sha }} .
- name: Trivy scan (HIGH/CRITICAL -> fail)
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'table'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
exit-code: '1'
deploy_test:
runs-on: ubuntu-latest
needs: container_scan
steps:
- uses: actions/checkout@v4
- name: Start test stack (Docker Compose)
env:
GIT_SHA: ${{ github.sha }}
run: |
docker compose -f docker-compose.test.yml up -d
echo "Waiting for health..."
# 等到 web 服務健康或逾時
for i in {1..30}; do
if [ "$(docker inspect -f '{{json .State.Health.Status}}' web 2>/dev/null)" = "\"healthy\"" ]; then
echo "Service is healthy."
break
fi
sleep 5
done
docker ps
docker compose -f docker-compose.test.yml ps
- name: Show logs (for debugging if needed)
if: failure()
run: |
docker compose -f docker-compose.test.yml logs --no-color > compose-logs.txt || true
shell: bash
- name: Upload compose logs
if: always()
uses: actions/upload-artifact@v4
with:
name: compose-logs
path: compose-logs.txt
if-no-files-found: ignore
dast:
runs-on: ubuntu-latest
needs: deploy_test
steps:
- name: Run ZAP Baseline in same docker network
run: |
# 讓 ZAP 容器加入 compose 網路,直接掃服務主機名 "web"
docker run --rm -t \
--network devsecops-testnet \
owasp/zap2docker-stable \
zap-baseline.py -t http://web:8000/ -m 5 \
-J zap-report.json -r zap-report.html -a
- name: Upload ZAP reports
uses: actions/upload-artifact@v4
with:
name: zap-report
path: |
zap-report.json
zap-report.html
- name: Teardown test stack
if: always()
run: |
docker compose -f docker-compose.test.yml down -v
🔹SAST 在 Commit / PR 階段就能回饋給開發者。
🔹依賴掃描 + 容器掃描 保證交付物(Artifact)沒有已知漏洞。
🔹DAST 在測試環境做黑箱測試,檢查實際運作風險。
🔹Sealed Secrets 則確保敏感資訊不會明文出現在 Repo。
1️⃣ app/main.py
一個最簡單的 FastAPI API,方便被掃描與部署。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello DevSecOps"}
2️⃣ requirements.txt
最小依賴清單,讓 pip-audit 可以掃描套件漏洞。
fastapi==0.111.0
uvicorn==0.30.0
3️⃣ Dockerfile
一個基礎的容器化檔案,讓 CI 可以 build Image 後交給 Trivy 掃描。
# 使用 Python 官方基底映像
FROM python:3.12-slim
# 設定工作目錄
WORKDIR /app
# 複製 requirements 並安裝
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 複製應用程式
COPY app/ ./app
# 預設執行 Uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
👉首先,我們要讓專案具備最基本的掃描條件。建立一個專案資料夾(例如 devsecops-demo-app/),裡面放好應用程式原始碼(例如 app/main.py)、依賴清單 requirements.txt 以及 Dockerfile。接著在 .github/workflows/ 目錄下新增一個 secure-ci.yml,貼上我們的 CI/CD Pipeline 配置。這樣當開發者 push 或發 PR 時,GitHub Actions 就會自動啟動 SAST(Bandit) 與 依賴掃描(pip-audit)。Bandit 負責靜態檢查程式碼中的弱點,而 pip-audit 則會針對 requirements.txt 檢查套件是否有已知漏洞。完成這一步後,專案的「基礎安全檢查」就能在程式碼提交時自動進行,並且回饋到 Pull Request 介面。
1️⃣容器掃描
(1)準備 Dockerfile
(2)CI 中自動執行 Trivy 掃描
在 GitHub Actions Workflow 中,container_scan job 會:
1.docker build -t myapp:${{ github.sha }} .
2.用 aquasecurity/trivy-action 掃描 Image
▪ 設定 severity: CRITICAL,HIGH
▪ 設定 exit-code: 1(發現漏洞就 fail)
👉 這一步的重點:確保映像檔不帶已知重大漏洞上線。
2️⃣測試部署
加入 Docker Compose 測試部署檔(docker-compose.test.yml):
name: devsecops-testnet # 固定網路名稱,方便 ZAP 同網路掃描
services:
web:
build:
context: .
dockerfile: Dockerfile
image: myapp:${GIT_SHA:-local}
container_name: web
ports:
- "8000:8000" # 如不需對外暴露,可拿掉
environment:
UVICORN_HOST: "0.0.0.0"
UVICORN_PORT: "8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
🔔重點
🔹name: devsecops-testnet:讓 Compose 網路名稱固定,ZAP 容器可以用 --network devsecops-testnet 掛進同網路。
🔹image: myapp:${GIT_SHA}:CI 裡用同一個 Tag($GITHUB_SHA)掃過 Trivy 的 Image 來部署。
🔹healthcheck:讓 CI 可以等到服務健康再掃描。
3️⃣動態應用測試
▪ 使用 OWASP ZAP Baseline 掃描,在 CI job 中會自動執行:
docker run --rm -t owasp/zap2docker-stable \
zap-baseline.py -t http://test-env/ -m 5 \
-J zap-report.json -r zap-report.html -a
🔹-t 指定測試環境 URL
🔹-m 5 最多掃描 5 分鐘(可調整)
🔹-J 輸出 JSON 報告
🔹-r 輸出 HTML 報告
🔹-a 嚴格模式:發現 Alert 就讓 job fail
👉接下來,我們要確保應用程式在容器化與部署後依然安全。CI 流程會先用 Dockerfile 建立映像檔,然後透過 Trivy 自動掃描,若偵測到 High 或 Critical 等級的漏洞,PR 就會被阻擋,避免有問題的 Image 被推送出去。當容器掃描通過後,Pipeline 會自動把應用程式部署到測試環境(可依你的環境選擇 kubectl、Helm 或 docker compose)。在測試環境跑起來後,再使用 OWASP ZAP Baseline 進行動態應用測試(DAST),模擬實際攻擊行為,檢查是否有 SQL Injection、XSS 等風險。最後會產出 HTML/JSON 報告,讓團隊可以檢視並修正。這個階段確保應用程式不是只在程式碼層安全,而是整個「打包 → 部署 → 運行」的鏈條都經過檢驗。
A. 機敏資訊保護(Sealed Secrets → secrets_apply job)
1️⃣建立原始 Secret(本地執行,不要上傳 Git)
kubectl create secret generic db-secret \
--from-literal=DB_USER=admin \
--from-literal=DB_PASS=supersecret \
-o yaml > db-secret.yaml
2️⃣使用 kubeseal 將 Secret 加密kubeseal < db-secret.yaml > k8s/secrets/sealed-secret.yaml
3️⃣在 GitHub Actions 中套用 Sealed Secrets
Workflow 會在 main 分支合併後,自動執行:kubectl apply -f k8s/secrets/sealed-secret.yaml
✅確保部署時會還原成真實 Secret。
B. 設定 PR Gate(Branch Protection)
1️⃣到 GitHub Repo → Settings → Branches → Add Rule。
2️⃣指定分支(例如 main),勾選:
✔️ Require a pull request before merging
✔️ Require status checks to pass before merging
3️⃣在 status checks 清單中,至少勾選:
✔️sast (Bandit)
✔️deps (pip-audit)
✔️container_scan (Trivy)
(可選:dast)
4️⃣測試:建立一個 PR,刻意引入漏洞(例如在 requirements.txt 加一個有漏洞的套件),確認 CI fail 後 PR 無法合併。
👉除了程式碼與容器的安全,機敏資訊也是重要環節。這裡我們使用 Sealed Secrets,先將敏感資訊(例如 DB 密碼、API Key)加密後存放在 k8s/secrets/sealed-secret.yaml,並在 CI 中自動套用。這樣就算 Repo 被公開,敏感資訊也不會以明文外洩。
同時,我們要設定 PR Gate,也就是在 GitHub 的 Branch Protection 規則裡要求 PR 必須通過特定安全檢查(例如 SAST、依賴掃描、容器掃描)才能合併。如果 Bandit 掃到高風險問題,或 Trivy 偵測到嚴重漏洞,PR 就會直接被擋下。這樣能確保任何進到主分支的程式碼與 Image,在安全上都經過把關。
A. 執行與檢視
1️⃣ 建立/更新 PR → 到 PR → Checks 看各 job 狀態與 Logs。
2️⃣ 於 Artifacts 下載報告:bandit-report.json、pip-audit.sarif、zap-report.html/json、compose-logs.txt。
3️⃣ 到 Repo → Security → Code scanning alerts 集中看 pip-audit.sarif 告警(CVE、修補建議)。
4️⃣ 修正後在 PR 右上角 Re-run jobs 或推新 commit 重新驗證。
B. Gate 調整(依團隊嚴格度)
1️⃣ Bandit(SAST):只擋 HIGH(預設)/ 連 MEDIUM 也擋
# sast job:HIGH+MEDIUM 一起擋(要寬鬆就只留 'HIGH')
python - << 'PY'
import json, sys
r = json.load(open('bandit-report.json'))
bad = [i for i in r.get('results', []) if i.get('issue_severity') in ('HIGH','MEDIUM')]
sys.exit(1 if bad else 0)
PY
2️⃣ Trivy(Image):擋哪些等級
# 寬鬆:只擋 CRITICAL
severity: 'CRITICAL'
exit-code: '1'
# 嚴格:連 MEDIUM 也擋
severity: 'CRITICAL,HIGH,MEDIUM'
exit-code: '1'
3️⃣ ZAP(DAST):是否擋 PR
# 產報告、不擋(去掉 -a)
zap-baseline.py -t http://web:8000/ -m 10 -J zap.json -r zap.html
# 嚴格 Gate(有 Alert 就 fail)
zap-baseline.py -t http://web:8000/ -m 10 -J zap.json -r zap.html -a
4️⃣ pip-audit(依賴):只上報 or 當 Gate
# 只上傳 SARIF(不上 Gate) → 保持你目前做法
# 要當 Gate(找到漏洞就 fail)
pip-audit -r requirements.txt
C. 讓 Gate 真的「擋合併」
1️⃣ Repo → Settings → Branches → Add rule(main)
2️⃣ 勾:✔ Require a pull request before merging、✔ Require status checks to pass before merging
3️⃣ 指定必過 checks:sast、container_scan(選用:deps、dast 看你 Gate 設定)
👉最後,當這條 Pipeline 跑起來後,我們需要學會如何檢視與調整門檻。執行一個 PR 後,可以在 PR 的 Checks 頁面看到每個 Job 是否通過,也可以在 Artifacts 下載報告(如 bandit-report.json、zap-report.html),或者到 Security → Code scanning alerts 集中檢視 pip-audit 的掃描結果。如果發現偵測太嚴格導致 PR 經常被擋,可以調整 YAML:例如只讓 Bandit 在偵測到 High 風險時失敗,Medium 風險則給警告;Trivy 只針對 Critical 與 High 報錯,其他則允許合併。反之,如果團隊想要更嚴格,也可以把 Medium 級別納入 Gate。透過這些設定,我們能讓安全檢查真正符合團隊需求,而不是流於形式。
DevSecOps 的精髓在於:
▪ 安全不是最後才加上的檢查,而是貫穿 CI/CD 的每一環。
▪ 從程式碼提交、建構、測試到部署,安全機制都應自動化執行。
▪ 工具可以更換,但思維是一致的:「安全左移」+「自動化」。
👉 下一篇 Day 30|最終回顧 × DevSecOps 展望