從 Zeabur 事件出發:保管別人的機密要防什麼

2026-10-22 · 3,379 字 · 7 分鐘

保管別人的機密 — 第 1 篇:起點

2026 年 10 月,Zeabur 公布了一份資安事件初步報告。

關於報告的備份版本

Zeabur 已宣布將於 2026 年 12 月 31 日結束營運,原報告頁面之後可能無法存取。因此我們製作了一份備份版本(PDF,2026 年 10 月 22 日存檔),作為文章情境的補充。系列中引用的事件細節,都可以在這份備份裡對照。

每次看到這種重大事件,我大多會回頭檢查自己的知識儲備夠不夠:如果換成是我,事前能不能實作好?營運中怎麼定期稽核?出事之後怎麼補救?

這次的問題是:若有一天,我要提供一個服務,得替使用者保管機敏資料,例如資料庫密碼、API 金鑰這類環境變數,該怎麼把它寫好?

這個系列就是在回答這個問題。Zeabur 在這裡只是對照組,真正的主角是「一個合格的機密保管服務應該長什麼樣子」。五篇共用同一張主圖,每篇換一個視角,讀完至少能對整套設計有個概念。

簡述 Zeabur 報告

事件的完整經過,建議直接讀原文或上面的備份版本,這裡只挑多數人讀到時最驚訝的一件事:平台資料庫裡存著 3,218,965 筆環境變數,名稱和值都是明文。攻擊者用一個內部唯讀帳號就全部讀走了。

會驚訝,是因為環境變數裡裝的正是資料庫密碼、API 金鑰、第三方服務的憑證。多數人把它們交給平台時,會以為平台有加密;實際上,任何能對這張表下 SELECT 的帳號,都能看到所有使用者的所有機密。

報告裡的其他問題,例如沒有期限的叢集管理權杖、收錄所有機密的每日備份,第 4 篇講到對應的設計時再拿出來對照。

以應該保護的 DB 連線資訊為例

為了不讓討論飄在空中,整個系列都用同一筆機密當例子。主角是 Org A,Org ACME 的簡稱,一家什麼都做的虛構公司(A Company that Makes Everything)。後面需要第二個客戶來對照時,就叫它 Org B。

Org A 在平台上部署了一個服務,服務需要連資料庫,所以 Org A 在後台填了一個環境變數 DATABASE_URL = postgres://app:s3cret@db:5432/main。

平台要做的事情聽起來很單純:把它存起來,每次 Org A 的容器啟動時,把這個值放進容器的環境變數。

最直覺的做法,是在資料庫開一張表,一欄放名稱、一欄放值。這就是 Zeabur 的做法,也是大部分人第一次寫會寫出來的樣子。它的問題是:任何能讀這張表的人,就等於能讀所有使用者的所有機密。內部唯讀帳號、備份檔、一次 SQL injection、一個手滑多給權限的工程師帳號,全部都通往同一個結果。

這個系列的每一篇,都在回答同一個問題:DATABASE_URL 這個字串,從 Org A 按下儲存,到它出現在容器裡,中間每一步該怎麼保護?

情境總覽:資料生命週期的旅程

下面這張圖是整個系列的地圖。後面每一篇都會再出現一次,只是亮起來的部分不同。

主圖:九個參與者、十五個步驟的循序圖,分成寫入與執行時兩段,並標出攻擊者可能碰到的四個位置 A 到 D

點圖可以開大圖。實線是請求,虛線是回應;藍色是跟 KMS 的往來,紅色是明文出現的地方。紅色圓圈 A 到 D 是攻擊者可能碰到的位置。

圖上有九個參與者,從左到右:

流程分成兩段。寫入是第 1 到 8 步:Org A 送出機密,經過加密存進資料庫,再被備份。執行時是第 9 到 15 步:容器要啟動,注入元件替它把機密拿出來、解開、交給它。

步驟編號全系列固定。第 2 篇說「第 12 步」,跟第 3 篇說的「第 12 步」是同一件事。現在看不懂每一步在做什麼沒關係,後面會一段一段拆開講。

攻擊者會從哪裡進來

主圖上的 A 到 D,是四個最常見的突破口。用天真的設計(明文存在資料庫)對照,看看各自會發生什麼:

位置 攻擊者拿到什麼 天真設計下的結果 希望的結果
A 平台 DB 整個資料庫的內容,例如 SQL injection、資料庫憑證外洩 所有機密直接外洩 只拿到解不開的密文
B 備份 備份檔 所有機密直接外洩 跟 A 一樣,只有密文
C 內部帳號 工程師或內部服務的帳號 能查資料庫就能讀全部 讀得到密文,但沒有權限解開
D 控制面 叢集管理權杖 能進任何容器、讀任何設定 權杖很快過期、從外面用不了,被用了會馬上被發現

注意 D 跟其他三個不一樣。A、B、C 都是「拿到存起來的東西」,加密可以擋住。D 是「拿到能操作系統的權限」,攻擊者可以直接進容器,而容器裡本來就有明文。這一類加密幫不上忙,要靠別的手段,這也是 Zeabur 這次的主要入口。第 4 篇會專門處理它。

三個設計目標

把上面那張表的最後一欄濃縮,就是這個系列要達成的三個目標:

  1. 拿到資料庫,不等於拿到機密。資料庫、備份、log 裡只有密文,鑰匙放在別的地方。
  2. 能解開機密的身分越少越好,而且每次解開都留下紀錄。平台上大部分的人和服務,根本不需要看到使用者的機密。
  3. 每一把鑰匙、每一個權杖都會過期,外洩時換得掉,被濫用時看得到。

第一個目標靠加密,第二個靠權限設計,第三個靠生命週期管理和偵測。這三個目標對應到下面的五層設計清單。

用時間軸來看:事前、營運中、事後

開頭那三個問題,剛好是看待這件事的三個時間點。這個系列也照這個順序走:

時間點 要回答的問題 在哪幾篇
事前 系統要怎麼設計、怎麼實作,才不會一被入侵就全部外洩 第 2 篇儲存層、第 3 篇存取層、第 4 篇控制面與生命週期
營運中 系統上線之後,怎麼定期確認設計還成立:權限有沒有慢慢變寬、憑證有沒有過期、告警還會不會響 第 5 篇前半
事後 真的出事時,怎麼在最短時間內發現、止血、補救 第 5 篇後半

設計得再好,上線一年後也會走樣:新同事多拿了權限、某把金鑰忘了輪換、某條告警因為太吵被關掉。Zeabur 的管理權杖從 2024 年建立後就沒有輪換過,八月事件後的輪換也只做了一半,問題都出在營運中這一段。所以這個系列不只講怎麼設計,也講上線之後怎麼維持。

五層設計清單(也是這個系列的目錄)

每一層先列出最關鍵的幾條,細節留給各篇:

層 最關鍵的做法 對應的目標
儲存 用 DEK 加密資料、KEK 加密 DEK,KEK 只待在 KMS 1
存取 API 只能加密不能解密;Decrypt 權限只給注入元件;明文存了就讀不回來 2
控制面與生命週期 管理權杖短期有效、不對公網;備份金鑰分開保管;機密可以輪換、到期、撤銷 3
稽核、偵測與應變 定期對帳權限與憑證清單;大量解密、容器內執行指令要告警;外洩時先切斷再輪換 3

知識的源頭來自於經典的設計

這個系列比較像一份文獻整理,每個環節都來自現成的文件:

這個系列做的事,是把這些散在各處的做法串成一條從頭走到尾的流程,再用 Zeabur 的實際經過檢查每一層擋不擋得住。目標是先完整看過一次、有個概念,真的要實作或稽核時,知道該去翻哪一份文件。完整的參考資料整理在第 5 篇。

名詞

後面幾篇會一直用到這些詞,忘記時回來這裡查:

名詞 意思
明文 沒加密、直接看得懂的原始內容,例如 postgres://app:s3cret@...
密文 加密後的內容,沒有鑰匙看不懂
DEK Data Encryption Key,資料加密金鑰。拿來加密實際資料的那把
KEK Key Encryption Key,金鑰加密金鑰。不碰資料,只拿來加密 DEK
KMS Key Management Service,金鑰管理服務。保管 KEK 的地方,例如 AWS KMS、GCP Cloud KMS
sealed box 一種用公鑰加密的格式,GitHub 用它在瀏覽器端先把機密加密再送出
workload identity 服務自己的身分證明,由平台發給執行中的程式,通常短期有效
注入元件 在容器啟動時解開機密、交給容器的元件,例如主機上的 agent。屬於資料面,依照控制面發給它的身分做事
控制面 control plane。負責「決定事情」的那一層:容器要在哪裡跑、設定是什麼、誰有什麼權限。容器編排器(例如 Kubernetes 的 API server、scheduler)就是控制面的一部分
資料面 data plane。負責「實際做事」的那一層:真正在跑的容器,以及主機上替它們做事的元件
稽核紀錄 誰在什麼時候做了什麼的紀錄,事後追查用

下一篇

主圖上的第一個目標,「拿到資料庫不等於拿到機密」,要怎麼做到?下一篇從一封實體的信開始講信封加密,最後在瀏覽器裡實際加密一次 DATABASE_URL。

下一篇:儲存層,信封加密 →