包包baobaolin.com
date
entry
023
topic
tooling
rev

你的防護措施從來沒有被觸發過

部署腳本開頭那道身分閘門,只有在身分錯的時候才會執行——而那條路徑,平常沒有人走。昨天我故意用錯的身分跑了一次,兩分鐘內發現兩個問題。

我在前一篇談 AWS 帳號時建議過一段閘門,放在任何會改動東西的腳本開頭:

EXPECTED_ACCOUNT=111111111111
ACTUAL=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL" != "$EXPECTED_ACCOUNT" ]; then
  echo "WRONG ACCOUNT: $ACTUAL" >&2
  exit 1
fi

這段程式碼在我的部署腳本裡跑了很多次,每次都通過。它看起來運作良好——但「通過」是它唯一被驗證過的行為。

問題一:它放行 root

帳號 ID 識別的是帳號,不是身分。而 root 使用者、每一個 IAM user、每一個 role,都屬於同一個帳號。

$ aws sts get-caller-identity
{
  "Account": "060795942439",
  "Arn":     "arn:aws:iam::060795942439:root"
}

Account 完全相符,閘門放行。也就是說:那段用來確保「不要跑在錯的地方」的檢查,對「用權限過大的身分執行」這件事完全沒有意見

要擋住身分而不只是帳號,比對的對象要換成 ARN:

EXPECTED_ARN="arn:aws:iam::060795942439:user/deploy"
ARN=$(aws sts get-caller-identity --query Arn --output text) || exit 1
[ "$ARN" = "$EXPECTED_ARN" ] || { echo "身分不符: $ARN" >&2; exit 1; }

差別只有一個查詢欄位。但比對帳號時,一個誤用 root 的部署會安靜地成功;比對 ARN 時,它會停下來。

問題二:錯誤訊息自己爆掉

第二個問題更荒謬。我改寫後的訊息長這樣:

echo "       預期     $EXPECTED_ARN(profile: $AWS_PROFILE)。中止。" >&2

實際跑出來是:

ERROR: 目前身分是 arn:aws:iam::060795942439:root
./scripts/deploy.sh: line 29: EXPECTED_ARN?: unbound variable

全形括號 緊接在變數名後面,bash 沒有把它當成變數名的結束符,於是去找一個叫 EXPECTED_ARN( 的變數。找不到,而腳本開著 set -u,於是整行變成 unbound variable 錯誤。

修法是用大括號界定:${EXPECTED_ARN}(profile: ${AWS_PROFILE})。半形括號不會有這個問題,所以中文訊息才踩得到。

結果上這次沒有釀成災難——閘門仍然擋下了執行,只是退出的方式是 bash 的錯誤而不是我寫的說明。但這正是重點:那一行只在「閘門要擋人」的時候執行,而在那之前,它從來沒有被執行過。

只在失敗時執行的程式碼,可以壞掉好幾年而沒有任何症狀——因為它的症狀只在你最需要它的那一刻出現。

負面測試就是故意做錯一次

找出這兩件事沒有用到任何工具,就是用錯的身分跑一次:

AWS_PROFILE=default ./scripts/deploy.sh

驗收標準有兩條,兩條都要滿足:

  1. 它有擋下來——沒有任何東西被寫到線上
  2. 它是用你寫的那句話擋下來的——不是 bash 的錯誤、不是 stack trace、不是空白

第 2 條是多數人會跳過的。看到「有擋住」就滿意了,但如果擋住的方式是崩潰,那麼下一個踩到的人拿到的是一則看不懂的訊息,而不是「你用錯身分了,該用哪個」。

還有哪些只在失敗時執行的程式碼

同一類的東西比想像中多。我會逐一實際觸發看看的是:

  • 錯誤處理分支——尤其是「這裡理論上不會發生」的那些
  • 告警通知——alert 規則設好了,但那條 webhook 真的送得出去嗎?收件者還在職嗎?
  • rollback 路徑——上線流程演練過很多次,退版流程通常一次都沒有
  • 404 與錯誤頁——它回的狀態碼對嗎
  • 備份還原

最後一條值得單獨說:沒有還原過的備份不算備份,只是一個佔空間的檔案。備份腳本每天成功執行、每天寫出檔案、監控一片綠燈,這些都不構成「資料救得回來」的證據。唯一構成證據的是真的還原過一次。

做內部系統導入時,我把「還原演練」寫進交付項目而不是留在維運建議裡。理由很現實:只要它不是交付項目,它就永遠排在下一季。

成本很低,所以沒有藉口

這次的負面測試花了大約兩分鐘:改一個環境變數、跑一次、看四行輸出。而它找出的兩個問題,其中一個讓整道閘門形同虛設。

把它變成一個習慣不需要框架——寫完任何一段「出事時會擋下來」的程式碼,當下就讓它出事一次。那是它離被驗證最近的一刻,之後你就會忘記自己有寫過這段。

如果只記得一件事

檢查本身也需要被檢查。 一個從來沒有回報過問題的檢查,有兩種可能:一切都好,或者它壞了。這兩種在儀表板上長得一模一樣,而分辨它們的唯一方法,是故意製造一次它應該要抓到的問題。

修訂紀錄

  1. 首次發布