工程文章

雲端 Mac CI 的 Git 憑證隔離與結束清理實作

雲端 Mac CI 的 Git 憑證隔離與結束清理實作

當一台長期運作的雲端 Mac 接連執行多個儲存庫的建置時,最危險的情況不一定是權杖直接出現在指令碼裡,而是上一個作業悄悄把憑證留給下一個作業。常見的殘留位置包括 Git 全域設定、內含驗證資訊的遠端位址、登入鑰匙圈,以及未刪除的暫存指令碼。處理這類問題的目標,不是「建置結束後刪除一個變數」,而是從一開始就將憑證限制在單次作業的邊界內。

先確認憑證會儲存在哪裡

不要急著修改流水線。首先應在不輸出機密值的前提下,確認 Git 實際讀取了哪些設定。--show-origin 會同時顯示設定來源,藉此區分系統層級、使用者層級與儲存庫層級的設定。

set +x

git config --show-origin --get-all credential.helper || true
git config --show-origin --get-regexp '^(credential|http)\.' || true
git config --show-origin --get-regexp '^url\..*\.insteadof$' || true

重點檢查以下三類跡象:

  • credential.helper 是否指向持久化鑰匙圈;
  • http.extraHeader 是否已寫入全域或儲存庫設定;
  • url.*.insteadOf 是否把一般位址改寫成內含驗證資訊的位址。

遠端位址也必須檢查,但不能原樣寫入日誌。只需判斷是否存在 通訊協定://使用者資訊@主機 結構;一旦發現就立即讓作業失敗,而不是輸出完整位址。

remote_url="$(git remote get-url origin)"
case "$remote_url" in
  *://*@*)
    printf '%s
' "Remote URL contains embedded credentials" >&2
    exit 1
    ;;
esac

稽核指令碼的首要原則,是只回報「可能存在憑證」,不要為了證明問題而把憑證本身輸出到建置日誌。

為每個作業建立獨立的 HOME

Git 會根據 HOME 推導使用者層級設定的路徑。若所有作業共用執行使用者的 HOME,就等同於共用 .gitconfig、憑證 helper 設定,以及大量工具狀態。更穩妥的做法,是為每次作業建立權限為 700 的暫存 HOME,並明確指定 Git 的全域設定檔。

set -eu

ORIGINAL_HOME="$HOME"
JOB_HOME="$(mktemp -d "${TMPDIR%/}/git-job.XXXXXX")"
chmod 700 "$JOB_HOME"

export HOME="$JOB_HOME"
export XDG_CONFIG_HOME="$JOB_HOME/.config"
export GIT_CONFIG_GLOBAL="$JOB_HOME/.gitconfig"
export GIT_TERMINAL_PROMPT=0

mkdir -p "$XDG_CONFIG_HOME"

這種做法不會隔離系統層級的 Git 設定,因此仍須主動重設 helper 鏈。Git 支援多個 credential.helper 值;先寫入空值,可以清除從較低優先層級設定繼承而來的 helper,再加入本次作業自己的實作。

不要把儲存庫也放進暫存 HOME

HOME 是憑證與工具狀態的邊界,無須同時充當工作區。原始碼目錄應繼續由 runner 的工作區管理,方便控管容量並收集產出物。兩者分開之後,清理 HOME 不會誤刪建置結果,清理工作區時也不會遺漏使用者層級的驗證設定。

若流水線需要並行執行,每個並行槽都必須各自呼叫一次 mktemp,不能依儲存庫名稱重複使用固定目錄。固定目錄在異常結束後可能被下一次作業繼承,也可能讓兩個作業同時修改同一份 .gitconfig

僅在 Git 要求時提供權杖

權杖應透過 CI 的機密變數機制注入環境,指令碼只引用變數名稱。不要把權杖拼接到 clone 位址中,也不要使用 git config --global http.extraHeader 長期儲存驗證標頭。

以下 helper 只回應 Git 的 get 要求;設定檔中儲存的是引用變數的邏輯,而不是權杖值:

: "${CI_GIT_USER:?CI_GIT_USER is required}"
: "${CI_GIT_TOKEN:?CI_GIT_TOKEN is required}"

git config --global credential.helper ""
git config --global --add credential.helper \
'!f() {
  if [ "$1" = get ]; then
    printf "username=%s
password=%s
" \
      "$CI_GIT_USER" "$CI_GIT_TOKEN"
  fi
}; f'

關閉命令回顯與互動式後援機制

接觸機密變數前必須執行 set +x,否則 shell 會把變數展開後的命令寫入日誌。GIT_TERMINAL_PROMPT=0 同樣重要:權杖缺失或失效時,作業應明確失敗,不能停在不可見的互動式提示上。

還必須檢查建置包裝程式是否會自動傾印環境變數。診斷資訊只應顯示變數是否已設定,例如檢查字串長度是否大於零;不應輸出變數內容、驗證標頭或完整的遠端位址。

對於需要寫入權限的自動化工作,應使用不同憑證分別讀取原始碼與推送產出物。只執行 checkout 的作業不需要寫入權限;發布作業也不應取得超出目標儲存庫與必要操作範圍的權限。

使用 trap 涵蓋所有結束路徑

只在指令碼結尾執行 rm -rf,無法涵蓋中途失敗、逾時終止或人工取消。應在建立暫存目錄後立即註冊 trap,並在清理函式中先清除變數,再刪除作業 HOME。

cleanup_git_credentials() {
  set +e
  unset CI_GIT_TOKEN CI_GIT_USER
  if [ -n "${JOB_HOME:-}" ] && [ -d "$JOB_HOME" ]; then
    rm -rf "$JOB_HOME"
  fi
}

trap cleanup_git_credentials EXIT HUP INT TERM

清理函式必須允許重複執行,即使目錄已不存在也不能報錯。刪除目標還必須符合兩項條件:變數不可為空,而且確實指向目錄。不要使用範圍過廣的鑰匙圈刪除命令,也不要清空整個登入鑰匙圈,因為其中可能包含同一執行使用者所需的其他項目。

若舊有流水線曾使用持久化 helper,應先依主機與帳戶精確盤點,再安排一次性遷移。在新舊方案並行期間,每次作業都必須驗證目前 helper 的來源,避免暫存 HOME 雖已啟用,系統包裝指令碼卻又把舊設定複製回來。

將殘留檢查設為建置門禁

清理是否成功,不能只依據指令碼沒有報錯來判斷。建議在 runner 外層加入作業結束後的檢查,或讓執行器在回收作業時驗證以下項目:

  • 暫存 HOME 已刪除;
  • 工作區的 .git/config 中沒有驗證標頭,也沒有包含使用者資訊的遠端位址;
  • 作業日誌中沒有機密變數的已知指紋;
  • Git 設定來源只包含預期的系統設定與本次暫存設定;
  • 後續的空白作業無法讀取上一次作業的儲存庫憑證。

可以為測試權杖設定一個沒有任何權限的固定標記值,在隔離環境中執行失敗案例,再掃描日誌與檔案系統。這裡要檢查的是「標記是否洩漏」,而不是驗證真實權杖。失敗案例至少應涵蓋 clone 失敗、建置命令結束、收到終止訊號,以及重複執行清理函式。

當多個團隊共用同一個實體節點時,還應將執行使用者作為第二道邊界。暫存 HOME 解決的是作業層級殘留;獨立使用者則負責隔離不同信任域之間的行程、檔案權限與鑰匙圈邊界。兩層邊界同時存在,才不會把指令碼清理當成唯一防線。

最終驗收標準很簡單:作業開始前沒有可繼承的憑證,執行期間只在需要時提供,任何結束路徑都會進行清理,而且下一次作業無法證明上一枚權杖曾經存在。達成這些條件後,Git 憑證才真正屬於該次作業,而不是屬於那台長期運作的雲端 Mac。

常見問題

作業結束時只 unset 權杖變數是否足夠?

不足。憑證可能已由 helper 寫入鑰匙圈,也可能留在遠端網址或全域設定中,因此必須逐一檢查並清理。

長時間運行的 Mac CI 節點可以共用登入鑰匙圈嗎?

不同儲存庫或信任邊界不應共用。優先使用短效且最小權限的權杖,確有持久化需求時再隔離執行使用者或鑰匙圈。

獨享雲端 Mac

為開發、建置與遠端桌面選擇一台獨享實體機

比較三種 Apple Silicon 設定,並在下單時選擇節點、租用期間與儲存空間加購項目。

選擇租用方案