加密擋不住的路:控制面與生命週期
保管別人的機密 — 第 4 篇:控制面與生命週期
前兩篇做完之後,平台 DB、備份、內部帳號這三個突破口都擋住了:偷到的只有密文,能請 KMS 解密的只有注入元件。但第 1 篇就說過,D 控制面跟其他三個不一樣。這篇專門處理它,順便處理備份另外要注意的事,以及所有金鑰和權杖的生命週期。
控制面與備份的視角。亮起來的是第 8 步(備份)、第 9 步(控制面發身分)、第 15 步(明文進容器)。看完整主圖
能管理叢集的人,可以直接進容器拿明文
假設平台跑在 Kubernetes 上,攻擊者手上有一份 cluster-admin 權限的 kubeconfig。他不需要碰 KMS,下面任何一個指令都能拿到 Org A 的 DATABASE_URL:
# 1. 進到 Org A 的容器,印出環境變數
kubectl -n org-a exec deploy/web -- env | grep DATABASE_URL
# 2. 映像檔裡沒有 shell 也沒關係:掛一個 debug 容器,讀應用程式行程的環境變數
kubectl -n org-a debug -it pod/web-7d9f8 --image=busybox --target=web \
-- sh -c 'tr "\0" "\n" < /proc/1/environ'
# 3. 機密如果存成 Kubernetes Secret,直接讀出來(base64 只是編碼,不是加密)
kubectl -n org-a get secret web-env -o jsonpath='{.data.DATABASE_URL}' | base64 -d
# 4. 自己起一個 Pod,用 Org A 的 service account 跑,讓注入元件替它把機密解開
kubectl -n org-a run probe --image=busybox \
--overrides='{"spec":{"serviceAccountName":"web"}}' -- sleep 3600
前三個指令直接讀主圖第 15 步之後、已經在容器裡的明文。第 4 個指令走的是第 9 步:注入元件看到的是一個帶著 Org A 身分的 Pod,照規則替它解密,KMS 那邊的權限檢查也會通過,因為身分本身是真的。
這幾條路都不需要 KMS 的 Decrypt 權限,所以前兩篇的保護全部被繞過。Zeabur 這次就是這樣:攻擊者拿到共享叢集的管理權杖後,讀了所有服務的設定與機密,還在 865 個資料庫服務的容器裡執行指令。
上面這些指令打進去的地方,就是控制面(control plane);指令最後作用的地方,是資料面(data plane):
| 控制面(control plane) | 資料面(data plane) | |
|---|---|---|
| 負責 | 決定容器在哪跑、設定是什麼、誰有什麼權限 | 實際跑服務、處理流量 |
| Kubernetes | kube-apiserver、scheduler、controller-manager、etcd | 節點上的 kubelet、Pod,以及 Secrets Store CSI driver 這類注入機密的元件 |
| Fly.io | 排程 Machine,發臨時 token 給主機 | 主機上的 agent 用 token 解密、注入 Machine |
| 主圖 | 控制面這條 lane | 注入元件、容器 |
控制面不可能不存在,平台總得有人能管理叢集。所以這裡的目標不是「擋住」,而是讓管理權限:很難拿到、拿到了很快失效、從外面用不了、用了會被看見、能影響的範圍越小越好。
第一件事:管理權杖要短命
Zeabur 的共享叢集管理權杖 2024 年 1 月建立,沒有期限、從來沒有輪替,2026 年 1 月又被以明文寫進設定儲存庫,到 2026 年 10 月被攻擊者拿來用。換成時間軸來看,就知道「沒有期限」有多危險:
長期權杖外洩後能用多久?直到有人發現為止。短期權杖外洩後能用多久?最多到它過期。
做法上有幾個層次,越往下越好:
- 至少要有期限。任何管理權杖都設到期日,到期前輪換。
- 不要存在任何檔案裡。設定儲存庫、wiki、CI 變數都不行。需要時從機密管理服務取,用完不留。
- 用時才申請(just-in-time access)。平常沒有人持有管理權限。要操作時提出申請,寫明原因,核准後發一張一小時的權杖,時間到自動失效。每次申請都留紀錄。
- 跟人綁在一起。權杖透過公司的 SSO 加上 MFA 取得,而不是一串大家共用的字串。誰在什麼時候拿了權限,一目了然。
另外留一個緊急用的 break-glass 帳號,給 SSO 故障之類的狀況使用。它的憑證要封存保管,一旦被使用就立刻告警。
檢查自己的環境,可以從這幾個指令開始:
# kubeconfig 裡有沒有寫死的 token?有 token: 欄位,就是一份沒人管期限的憑證
grep -n "token:" ~/.kube/config
# 叢集裡有沒有長期有效的 service account token(舊版 Kubernetes 會自動建立這種 Secret)
kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
# 真的需要 token 時,發一張有期限的
kubectl -n ops create token deployer --duration=1h
第二件事:管理入口不對公網
Zeabur 的共享叢集管理介面當時對公網開放,攻擊者拿到權杖就能從世界任何地方連進來。事件發生後,他們第一批處理的事情之一,就是限制只有 Zeabur 自己的系統能連線。
管理入口放在私有網路裡,只能透過 VPN、跳板機或零信任代理進入。這樣攻擊者需要同時拿到兩樣東西:權杖,加上進入內網的管道,只偷到權杖還用不了。
確認方法很直接,從公司網路外面試連 API server:
curl -sk --max-time 5 https://<api-server>:6443/version
有回應就代表管理入口對外開放,權杖一外洩就能直接從外面使用。
第三件事:內部和使用者的東西分開
報告裡有一句很關鍵:內部服務和使用者服務部署在同一個叢集,攻擊者讀取使用者設定時,也一併拿到了內部服務的憑證。一份權限能讀的範圍,同時涵蓋了使用者和平台自己。
分開的方式有很多層:
- 平台自己的服務和使用者的服務放在不同叢集,甚至不同的雲端帳號。
- 正式環境和測試環境的機密分開保管,用不同的 KMS 金鑰、不同的權限。OWASP 的清單也特別列了這一條。
- 不同區域的叢集用不同的管理權杖。一把權杖外洩,影響一個區域,而不是九個。
分開的目的是縮小「一份憑證外洩時,能影響多少服務」。資安領域把這叫爆炸半徑(blast radius)。要知道一個帳號的範圍有多大,直接列出它能做的事:
kubectl auth can-i --list --as=system:serviceaccount:platform:deployer
如果同一個帳號既能讀 org-a 這類使用者 namespace,又能讀平台內部的 namespace,範圍就太大了。
第四件事:進容器這個動作要被看見
在容器裡執行指令(kubectl exec 這類)是讀取明文最直接的方法。平台工程師偶爾需要它來除錯,但它應該是:
- 權限分開管理,不是所有管理員都有。
- 每次執行都記錄誰、在哪個容器、執行了什麼。
- 對使用者的容器執行時,觸發告警。
在 Kubernetes 裡,exec 是獨立的子資源 pods/exec,可以單獨授權、單獨記錄:
# 某個人能不能對 org-a 的 Pod 執行 exec?
kubectl auth can-i create pods/exec -n org-a [email protected]
# kube-apiserver 的 audit policy:exec、attach、port-forward 一律記錄
# 執行的指令會出現在紀錄的 requestURI 裡
- level: Metadata
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
Zeabur 的攻擊者在資料庫容器裡活動了三個多小時,沒有觸發任何告警。他們事後列出的新告警項目裡,「在容器內執行指令」就是其中一項。第 5 篇會把這些告警整理成清單。
備份:另一條繞路
主圖第 8 步,平台 DB 每天備份一次。第 2 篇說過,如果資料庫裡只有密文,備份裡也只有密文,被偷了一樣解不開。但 Zeabur 的備份出了三個問題,每一個都值得記下來。
第一,叢集備份裡有所有機密的明文。注意這裡說的是叢集備份,不是平台 DB 的備份。報告沒有交代備份的格式,但這在 Kubernetes 上是很常見的坑:叢集的 Secret 物件預設存在 etcd 裡,只做了 base64 編碼,沒有加密。備份 etcd,就等於備份了所有 Secret 的明文。Kubernetes 本身提供 KMS provider,可以用跟第 2 篇一樣的信封加密來加密 etcd 裡的資料,這個要記得打開。打開之後,可以直接從 etcd 讀一筆 Secret 確認:
# 省略 etcdctl 的連線參數
etcdctl get /registry/secrets/org-a/web-env | hexdump -C | head -3
看得到 k8s:enc:kms:v2: 開頭,代表有加密;看得到 DATABASE_URL 這類欄位名稱跟值,代表是明文。
第二,備份的存取金鑰跟其他機密放在一起,而且權限太大。報告寫到,一把只需要讀一個儲存空間的 AWS 金鑰,實際上能讀整個 AWS 帳號的儲存空間。備份金鑰應該另外保管,權限只開到它負責的那個 bucket。在備份主機上用這把金鑰試一下就知道:
aws s3 ls s3://backup-tpe1/ # 應該成功
aws s3 ls s3://some-other-bucket/ # 應該是 AccessDenied
第三,也是最容易被忽略的一點:輪換過的新金鑰,被備份收錄了。
2026 年 10 月 15 日(UTC),依 Zeabur 報告整理。新憑證被收進備份,而攻擊者手上已經有備份的存取金鑰。
Zeabur 在事件中輪換了一批憑證,這是正確的動作。但下午 4 點的例行備份照常執行,把剛換好的新憑證收了進去,而攻擊者這時已經握有備份的存取金鑰。晚上 8 點半,攻擊者用上了新憑證。
從這裡學到兩件事:
- 備份裡不該有任何機密的明文。如果備份只有密文,新憑證被收錄進去也無所謂。
- 處理事故時,先切斷攻擊者已知的所有管道,再輪換。如果攻擊者還能讀備份,新憑證一產生就會再被他拿走。第 5 篇的應變順序會再講一次。
Zeabur 的改善措施裡也有這一條:叢集備份不再保存明文機密,備份金鑰與其他機密分開保存。
生命週期:每一份憑證都要能換
前面講的是控制面的權杖。把範圍放大,平台上所有的金鑰、權杖、憑證,都該有完整的生命週期:建立、使用、輪換、到期、撤銷。這分成兩群。
使用者的機密
Org A 的 DATABASE_URL 也會需要更換,例如資料庫密碼外洩了。平台要讓這件事很容易:
- 支援覆蓋與版本:更新後新容器拿新值,舊版本保留一段時間,方便回滾。
- 更新後要能觸發重新部署,否則舊容器記憶體裡還是舊值。
- 可以標註到期日,到期前提醒使用者。
- 能做到的話,鼓勵使用者改用短期憑證。例如資料庫支援的話,讓注入元件每次啟動時向資料庫要一組一小時有效的帳密,而不是保存一組永久密碼。OWASP 把這叫 dynamic secrets。
平台自己的憑證
Zeabur 的報告裡有一段更正很值得記住:八月事件後,他們說已經停用或輪換所有內部管理金鑰,實際上只換了 GitHub App 私鑰,設定儲存庫裡的叢集管理權杖和 AWS 金鑰都沒換。
會發生這種事,通常不是因為懶,而是因為根本不知道自己有哪些憑證。所以第一步是盤點,為每一把憑證建一筆紀錄:
| 憑證 | 負責人 | 存在哪裡 | 能做什麼 | 有效期限 | 上次輪換 | 怎麼輪換 |
|---|---|---|---|---|---|---|
| 叢集管理權杖(tpe1) | 平台組 | 不存放,JIT 申請 | 管理 tpe1 叢集 | 1 小時 | 每次申請 | 自動 |
| 備份 bucket 金鑰 | 平台組 | KMS 封存 | 只能寫 backup-tpe1 | 90 天 | 2026-09-01 | runbook #12 |
| sealed box 私鑰 | 安全組 | KMS | 解開送入的機密 | 180 天 | 2026-07-15 | runbook #7 |
表上的每一欄都是一個問題,答不出來的那一格就是風險。有了這張表,「輪換所有內部金鑰」才是一件能被驗證的事,而不是一句口號。
最後一個常被忘記的情況:服務停用時,它的憑證要跟著撤銷。Zeabur 有一把已停用服務的部署金鑰,停用四個月後仍然有效,事件中被拿來讀設定儲存庫。
對照主圖
- 備份:備份裡只有密文;叢集層的 etcd 也要加密;備份金鑰另外保管、權限只到一個 bucket。
- 控制面發臨時身分:控制面本身要用短期、跟人綁定、不對公網的權限來操作,而且操作會被記錄。
- 明文進容器:這裡的明文無法避免,所以進容器的動作(exec)要被限制、被記錄、被告警。
這一層擋住了什麼
| 位置 | 有了這一層之後 |
|---|---|
| A 平台 DB | 擋住(第 2 篇) |
| B 備份 | 擋住:備份裡沒有明文,備份金鑰權限縮小、另外保管 |
| C 內部帳號 | 擋住(第 3 篇) |
| D 控制面 | 沒有辦法完全擋住,但能把風險壓到很小:權杖很難拿到、拿到很快過期、從外面用不了、能影響的範圍變小 |
D 永遠不會變成「擋住」,這是誠實的答案。平台總得有人能管理叢集,能管理就能接觸到明文。剩下的最後一道防線,是在它被濫用的時候馬上發現。這是最後一篇的主題。
控制面與生命週期檢查清單
- [ ] 沒有無期限的管理權杖,平常沒有人持有管理權限
- [ ] 管理權限用時才申請、跟人綁定(SSO + MFA)、每次留紀錄
- [ ] 任何憑證都不存在設定儲存庫、wiki、CI 變數裡
- [ ] 管理入口只能從內網進入
- [ ] 內部服務與使用者服務分開叢集;正式與測試環境的機密分開
- [ ] 進容器執行指令的權限獨立管理,並且會告警
- [ ] 叢集層(etcd)的 Secret 有加密
- [ ] 備份裡沒有明文,備份金鑰另外保管、權限最小
- [ ] 使用者的機密可以覆蓋、有版本、能觸發重新部署
- [ ] 有一份平台憑證清單,每一把都有負責人、期限、輪換方式
- [ ] 服務停用時,它的憑證一起撤銷
下一篇
到這裡,事前的設計都講完了,四個攻擊位置也都處理過。但系統上線之後,每一層都會慢慢走樣,也可能直接出錯:權限越開越寬、清單漏了一把、某個人被釣魚。最後一篇講營運中怎麼定期稽核、出錯時怎麼發現、發現後怎麼補救,再用攻擊情境把前四篇逐一驗收。