包包baobaolin.com
date
entry
006
topic
tooling
rev

把 API token 放進 SSM Parameter Store,不要放 .env

.env 的問題不是它不安全,是它會被複製。談我現在改一筆 DNS 記錄的流程:token 從加密參數拉進 shell 變數、zone id 每次動態查、關掉終端機就沒了——以及為什麼「應該沒人看到」從來不是一個判準。

一條 Cloudflare API token 放在 .env 裡,權限是「這個帳號下所有 zone 的 DNS 寫入」。這個檔案在 .gitignore 裡,權限 600,沒有進版控。聽起來守住了。

然後你回想它這半年去過哪些地方:

  • 整個專案目錄被 rsync 到 NAS 當備份,那份沒有加密
  • docker build . 把整個目錄當 build context 送進 daemon,如果 .dockerignore 沒同步更新,它就在 image layer 裡
  • 交接時整包壓縮丟給同事,對方解壓在下載資料夾,到現在還在
  • 某次貼設定檔進聊天視窗問問題,滑鼠選取多框了三行

這四件事沒有一件是「安全機制失效」。檔案的本質就是會被複製,而複製不會通知你。問題不在 .env 的權限位元,在於它是一個檔案。

放哪裡

我用 AWS SSM Parameter Store 的 SecureString。它由 KMS 加密、有 IAM 控管、有 CloudTrail 稽核記錄,而且對這個用途是免費的。

Secrets ManagerSSM Parameter Store
月費$0.40/筆標準參數免費
API 費$0.05/萬次免費
自動 rotation有(Lambda hook)沒有,要自己排
適合DB 密碼、需定期輪替的一般 API token

需要自動 rotation 就用 Secrets Manager,其他情況 Parameter Store 夠了。一次性設定:

aws ssm put-parameter \
  --region ap-northeast-1 \
  --name /cloudflare/api-token \
  --type SecureString \
  --value 'cfat_PASTE_HERE' \
  --description "Cloudflare API token: DNS edit, all zones"

帳號 ID 這種不是機密但常要查的東西,用普通 String 存旁邊就好,不必混進加密參數裡。

每次操作的流程

拉進 shell 變數,不落盤:

CF_TOKEN=$(aws ssm get-parameter \
  --region ap-northeast-1 \
  --name /cloudflare/api-token \
  --with-decryption \
  --query Parameter.Value --output text)

先確認這條 token 還活著,再做任何會改東西的事:

curl -s -H "Authorization: Bearer $CF_TOKEN" \
  https://api.cloudflare.com/client/v4/user/tokens/verify \
  | jq '.success, .result.status'
# 預期 true / "active"

這一步值得做,因為 token 失效的錯誤訊息跟權限不足的錯誤訊息長得很像,先驗證可以省掉一輪猜測。

zone id 不要存

這是我覺得最容易被忽略的一點。每次操作前用域名查:

DOMAIN="example.com"
CF_ZONE_ID=$(curl -s -H "Authorization: Bearer $CF_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN" \
  | jq -r '.result[0].id')

[ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" = "null" ] && {
  echo "ERROR: zone $DOMAIN 不在這個帳號下"; return 1
}

三個理由:

  1. 加新域名不用改設定。存死 zone id 的話,每接一個新域名就要多加一條,遲早有人忘記。
  2. 它會漂。域名轉出再轉回、或搬到另一個 Cloudflare 帳號,zone id 就換了。存下來的那份不會報錯,只會安靜地指向一個你沒有權限的 zone。
  3. 那個查詢順便驗證了前提。查不到就代表「這個域名不在這個帳號下」,這件事你本來就該在改 DNS 前確認。

查一次 zone 是一個 HTTP round trip。用這個成本換掉一整類「設定檔與現實不一致」的問題,很划算。

不落盤的幾條紀律

  • echo $CF_TOKEN——一 echo 就進了 scrollback,而 scrollback 會被截圖
  • 不重導向到檔案,包括「暫時」的那種
  • 自動化腳本裡只透過環境變數傳,不要 inline 成字串——inline 的會進 process list,同一台機器上的其他人 ps 得到
  • 操作完關掉那個終端機分頁

順帶一提兩個 Cloudflare 自己的坑:DKIM 之類的驗證用 CNAME 一定要 proxied: false(橘雲關掉),開著會驗不過;還有 token 的 rate limit 是每 5 分鐘 1200 次,批次操作要自己 sleep。

什麼算洩漏

判準很簡單:只要那串字曾經出現在檔案、git、聊天視窗、或沒清掉的終端機捲動區,就算洩漏。不需要證明有人看到,也不用評估「應該沒事吧」。

會有人想省這一步,是因為直覺上 rotate 很麻煩。實際上它是:到 dashboard 按 roll、然後

aws ssm put-parameter \
  --region ap-northeast-1 \
  --name /cloudflare/api-token \
  --type SecureString \
  --value 'cfat_NEW_VALUE' \
  --overwrite

兩分鐘。既然成本這麼低,就不需要「判斷這次算不算真的洩漏」——那個判斷比 rotate 本身還貴,而且會判斷錯。

這是把要靠人判斷的地方換成規則的一種:規則會執行,判斷會偷懶。

代價

誠實講這套的缺點:

  • 每次操作多一次 AWS API 呼叫,大概幾百毫秒
  • 綁在 AWS 上。手邊沒有可用的 AWS 憑證時,你連 DNS 都改不了
  • token scope 開成「所有 zone」很方便,但失竊的破壞範圍也是所有 zone——要配 IP 白名單和到期日一起用

第二點是真的會擋到人的。我接受它,因為「改 DNS」本來就不該是隨手能做的事——需要先有一組有效的雲端身分,這個門檻是特性不是缺陷。

如果只記得一件事

憑證的風險跟它的權限成正比,跟它被複製過幾次成正比,跟你有多小心無關。把它從檔案系統移到一個需要身分才能取用、而且每次取用都留下記錄的地方,你就不用再依賴自己記得所有複製發生過的位置。

修訂紀錄

  1. 首次發布