雲端 Mac SSH 主機金鑰驗證與安全輪替實作

雲端 Mac SSH 主機金鑰驗證與安全輪替實作

開發者第一次連線至雲端 Mac 時,終端機通常會詢問是否接受未知主機。手動操作時輸入 yes 很快,搬到 CI 後改用 StrictHostKeyChecking=no 看似更省事,但兩種做法都略過了身分驗證。只要 DNS、位址設定或網路路徑遭到錯誤導向,憑證與建置資料就可能被送往錯誤的機器。正確做法是先透過可信任的終端機取得主機金鑰指紋,再限制開發機與自動化工作只接受這把金鑰。

先建立獨立的信任基準

主機金鑰用來證明「目前連線的確實是預期中的那台 Mac」,與使用者登入時使用的私密金鑰並不相同。以 Ed25519 主機金鑰為例,可以透過 XcodeVM 控制台提供的 Web VNC 開啟本機終端機,直接在目標機器上讀取公開金鑰指紋:

sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256

請記錄完整的 SHA256: 值,並一併記下執行個體識別碼、連線位址與讀取方式。不要只透過尚待驗證的 SSH 連線取得這個值,否則驗證流程會形成封閉迴圈。

接著在開發機上讀取遠端目前提供的公開金鑰:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > ~/.ssh/xcodevm_known_hosts.new
ssh-keygen -lf ~/.ssh/xcodevm_known_hosts.new -E sha256

逐字元比對兩邊的指紋。確認完全相符後,再安裝檔案:

install -m 600 ~/.ssh/xcodevm_known_hosts.new ~/.ssh/xcodevm_known_hosts
rm -f ~/.ssh/xcodevm_known_hosts.new

ssh-keyscan 解決的是「讀取公開金鑰」,不是「證明公開金鑰可信」。若沒有獨立取得的指紋作為基準,掃描結果不能直接寫入正式環境的信任檔案。

將 known_hosts 與專案隔離

全域 ~/.ssh/known_hosts 適合日常互動式連線,但團隊專案最好使用獨立檔案。如此一來,所有變更都能明確審查,也不會因個人清理紀錄而影響自動化工作。

~/.ssh/config 中建立別名:

Host xcodevm-build
    HostName 192.0.2.10
    User builder
    IdentityFile ~/.ssh/xcodevm_build_ed25519
    IdentitiesOnly yes
    UserKnownHostsFile ~/.ssh/xcodevm_known_hosts
    StrictHostKeyChecking yes
    HashKnownHosts yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

IdentitiesOnly yes 可避免用戶端依序嘗試過多身分金鑰檔案;StrictHostKeyChecking yes 則可確保遇到未知或已變更的金鑰時立即失敗。連線前應先檢查展開後的設定,避免別名遭其他規則覆寫:

ssh -G xcodevm-build |
  grep -E '^(hostname|user|identityfile|userknownhostsfile|stricthostkeychecking) '

如果同一個專案使用多台機器,應為每台機器保留獨立項目,並將執行個體識別碼寫入內部變更紀錄。不要用一個過於寬泛的別名指向可能隨機變動的位址。

在 CI 中固定指紋

CI 不應長期沿用某位開發者的個人信任檔案。更穩妥的做法是將預期的 SHA256 指紋儲存為受保護變數,讓每次工作先掃描並比對,再建立暫時的 known_hosts

set -euo pipefail

: "${MAC_HOST:?MAC_HOST is required}"
: "${EXPECTED_ED25519_SHA256:?fingerprint is required}"

scan_file="$(mktemp)"
known_hosts_file="$(mktemp)"
trap 'rm -f "$scan_file" "$known_hosts_file"' EXIT

ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > "$scan_file" 2>/dev/null
actual="$(ssh-keygen -lf "$scan_file" -E sha256 | awk '{print $2}')"

if [ "$actual" != "$EXPECTED_ED25519_SHA256" ]; then
  printf '%s
' "SSH host key verification failed" >&2
  exit 1
fi

install -m 600 "$scan_file" "$known_hosts_file"

ssh \
  -o UserKnownHostsFile="$known_hosts_file" \
  -o StrictHostKeyChecking=yes \
  -o IdentitiesOnly=yes \
  "$MAC_USER@$MAC_HOST" \
  'uname -m && sw_vers -productVersion'

私密金鑰、預期指紋與主機位址應分開管理。日誌可以輸出實際指紋以協助除錯,但不得列印私密金鑰內容、連線密碼,或未經遮蔽處理的環境變數。

正確處理金鑰變更

主機金鑰變更可能源自系統重設、重新產生主機金鑰或重複使用連線位址,也可能表示連線遭到導向錯誤目標。看到 REMOTE HOST IDENTIFICATION HAS CHANGED 時,應先停止自動化工作,不能直接刪除紀錄後重試。

安全輪替應依照以下順序進行:

  1. 在控制台確認執行個體識別碼、連線位址與近期操作是否符合預期。
  2. 透過 Web VNC 的本機終端機重新讀取 Ed25519 公開金鑰指紋。
  3. 由另一位維護者複核新舊指紋與變更原因。
  4. 掃描遠端公開金鑰並驗證新指紋,產生新的信任檔案。
  5. 先更新測試工作,完成一次唯讀連線與環境檢查。
  6. 再更新正式工作,並撤銷舊指紋。

若使用雜湊化紀錄,可以先找出舊項目:

ssh-keygen -F "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts
ssh-keygen -R "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts

第二個指令只能在新指紋已透過獨立管道確認後執行。刪除舊紀錄只是輪替步驟,不是驗證程序。

使用檢查清單完成驗收

上線前至少應檢查以下項目:

檢查項目 合格條件
指紋來源 從目標機器的本機終端機讀取
金鑰演算法 明確固定為 Ed25519,不接受任意演算法
檔案權限 .ssh 為 700,信任檔案為 600
用戶端策略 StrictHostKeyChecking=yes
CI 隔離 每個工作使用獨立的暫存檔案
變更處理 先停止工作、再核驗、最後替換
日誌內容 保留指紋與失敗階段,不洩漏憑證

此外還應執行一次負向測試:暫時將預期指紋改成錯誤值,確認工作會在真正執行遠端指令前結束。接著恢復正確值,再驗證唯讀指令、儲存庫存取與建置進入點。如此才能證明保護措施確實有參與執行,而不只是存在於設定檔中。

SSH 信任鏈不需要複雜的系統,關鍵在於把「首次接受」與「變更後重新連線」從臨時點選確認,轉換成可供審查的流程。可信任的終端機提供基準,專案檔案保存固定關係,CI 在每次連線前進行驗證,任何變更都必須先停下來核對。完成這些步驟後,遠端開發與自動建置才能共用同一套明確的主機身分邊界。

常見問題

ssh-keyscan 取得的主機金鑰可以直接信任嗎?

不可以。ssh-keyscan 只會讀取遠端當下提供的公鑰,仍須與透過本機終端等獨立可信任路徑取得的 SHA256 指紋比對,確認一致後才能加入信任。

主機金鑰改變後可以刪除 known_hosts 記錄再重新連線嗎?

不應把刪除記錄視為驗證。應先停止連線並確認位址與系統狀態,再從可信任終端重新讀取指紋,核對一致後才替換舊記錄。

CI 應該共用開發者個人的 known_hosts 嗎?

不建議。CI 應建立權限為 600 的工作專用檔案,明確啟用 StrictHostKeyChecking,並在工作結束後刪除暫存信任資料。

XcodeVM 雲端 Mac

選擇適合目前工作負載的獨享實體主機

核對記憶體、儲存空間、節點與計費週期後,即可進入下單設定。所有節點全年正常運作,實際可用性以控制台即時回傳的結果為準。

選擇方案並租用