為什麼 .env 不等於 Secrets Management?從 Zeabur 事件理解環境變數管理
從 Zeabur 環境變數事件理解 .env、部署平台與 Secrets Management 的差異,以及權限、稽核、rotation 和撤銷機制。
大家在開發專案時,應該都看過這類環境變數:
DATABASE_URL=...
OPENAI_API_KEY=...
JWT_SECRET=...通常我們會把它們放在 .env,或交給部署平台的 Environment Variables 功能管理。這比把密碼直接寫進 source code 好,但不代表 secrets management 已經做好。
最近 Zeabur 發生的環境變數資料未授權存取事件,讓這個問題變得很具體:當一個部署平台替很多專案集中保存 API key、資料庫密碼和 cloud credential(可以用來驗證身分並存取服務的憑證),這些資料就不只是設定值,而會成為攻擊者可以繼續利用的入口。
先從 env 和 secret 的差異開始,再看 secrets management service 補上了哪些能力。
Zeabur 事件發生了什麼事?
根據 Zeabur 官方事件說明,Zeabur 在 2026 年 8 月 28 日 07:11 UTC 發現專案環境變數資料遭到未授權存取。
官方目前確認的攻擊路徑如下:
- 攻擊者取得 Zeabur 洩漏的內部 AWS 管理員 credential。
- 使用該 credential 進入 Zeabur 位於 Tokyo region 的共用 AWS cluster(多個服務共用的運算環境)。
- 從 cluster 取得控制平面的 VPN 存取權限。控制平面是負責管理服務、設定和資源的管理層,通常比單一 application 能接觸到更多資料。
- 連線到 Zeabur 的主要資料庫。
- 對使用者的 project environment variables 進行特定查詢與匯出。
從攻擊者後續的操作來看,目標主要是 AI service API key 和其他可以直接使用的 credential。官方列出的變數類型包含:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
OPENAI_API_KEY
ANTHROPIC_API_KEY
GEMINI_API_KEY
OPENROUTER_API_KEY
GITHUB_TOKEN
STRIPE_SECRET_KEY
DATABASE_URL
MONGODB_URI
MYSQL_PASSWORD
POSTGRES_PASSWORD
REDIS_PASSWORD
JWT_SECRET
PRIVATE_KEY即使使用者沒有採用上述名稱,只要變數內容符合 AWS、GitHub、Anthropic、OpenRouter、OpenAI 或 Stripe credential 的格式,也可能被辨識為敏感資料。
Zeabur 目前確認有環境變數資料被針對性查詢與匯出,但尚未找到完整資料庫被取得,或其他 customer data 被大量匯出的直接證據。官方也表示第三方鑑識仍在進行中,後續結論可能會更新。
問題不在於某個 API key 被放進 env,而在於這些 credential 集中儲存在同一個控制平面後,可能造成很大的 blast radius,也就是單一事件擴散後可能影響的系統、服務和資料範圍。
.env 不等於 Secrets Management
.env 主要只是一種設定檔格式。它解決的是「程式從哪裡讀取設定」,並不會自動解決 secret 的完整生命週期。
例如,下面這個檔案可以讓 Node.js 讀取環境變數:
NODE_ENV=production
PORT=8080
DATABASE_URL=postgres://user:[email protected]/app但它沒有回答以下問題:
- 誰可以讀取
DATABASE_URL? - 這組資料庫密碼被哪些服務使用?
- 這組 credential 什麼時候建立?
- 什麼時候應該 rotation?
- 發生外洩時,能不能快速 revoke?
- 最近有哪些 application 讀取過它?
- staging 和 production 是否使用不同的 credential?
- 這個值有沒有被寫進 Git、Docker image 或 build log?
常見做法可以分成三個層次:
| 層次 | 主要解決的問題 |
|---|---|
.env | 讓程式讀取本地或部署環境的設定 |
| 部署平台的 environment variables | 在 build 或 runtime 注入設定 |
| Secrets manager | 管理權限、稽核、rotation、expiration 與分發 |
這三者的能力範圍不同。部署平台有可以填入 secret 的欄位,不代表它就具備完整的 secrets manager 能力。
先分清楚 build-time 和 runtime
環境變數常被混用,因為 build 和 runtime 使用的是不同階段的設定。build-time 指的是建置 application 時,runtime 則是 application 實際執行時。
Build-time environment variable
前端專案常見的 VITE_、NEXT_PUBLIC_ 變數,很多是在 build 時被 bundler 讀取,最後直接寫進 JavaScript bundle。
例如:
VITE_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=...這些值只要送到瀏覽器,就不再是秘密。使用者可以下載 bundle,或從瀏覽器的 Network、Source 和開發者工具中找到它們。
這也是我在前後端開發技術統整中提過的觀念:前端環境變數很多時候只是 build-time constant,不代表它是安全的 server secret。
下面這些通常可以是公開設定:
NODE_ENV=production
PORT=8080
VITE_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=...但下面這些不應該進入前端 bundle:
OPENAI_API_KEY=...
AWS_SECRET_ACCESS_KEY=...
STRIPE_SECRET_KEY=...
DATABASE_URL=...
JWT_SECRET=...如果前端需要呼叫第三方服務,通常應該讓後端代為呼叫,而不是把第三方服務的 secret 發給瀏覽器。
Runtime environment variable
Runtime env 是 application 啟動或執行時由平台注入的設定。這比 build-time env 更適合存放 server-side secret,但仍然不是完整的安全機制。
即使 secret 沒有被寫進 Git,它還是可能出現在:
- 部署平台的 dashboard
- CI/CD pipeline 設定
- Docker image layer
- build cache
- application log
- crash dump
- process inspection
- 備份資料庫
所以「沒有進 Git」只是基本要求,不是終點。
Secrets Management 真正要處理什麼?
OWASP Secrets Management Cheat Sheet把 secrets management 定義成一套包含 儲存、供應、稽核、rotation 和管理的流程,不只是把字串加密後放進資料庫。
1. 集中盤點 secret
專案變多之後,secret 很容易散落在不同地方:
.env檔案- GitHub Actions
- Docker Compose
- Kubernetes manifest
- 部署平台 dashboard
- CI/CD variables
- 訊息平台私訊
- 團隊成員的 password manager
專門的 secrets manager 可以將這些資料集中管理,並且建立 project、environment 和 service 的關係。
例如:
my-app
├── development
├── staging
└── production接著 application 只取得自己需要的值,而不是讀取整個團隊的 secret。
集中化讓我們比較容易追蹤 這組 key 被誰使用,也會讓 secrets manager 成為更有價值的攻擊目標。保存的資料越多,單一服務被攻陷時可能影響的專案就越多。
2. Least privilege
Least privilege(最小權限)是指只授予使用者或 application 完成工作所需的最低權限。不應該讓每一個工程師、CI job 或 application 都可以讀取全部 secrets。
比較合理的權限模型可能是:
api-production
├── read production database credential
├── read payment provider key
└── read jwt signing secret
worker-production
└── read queue credential
frontend-production
└── read public configuration only這些權限可以依照以下維度拆分:
- Project
- Environment
- Service
- Team
- User
- Machine identity
- Role
OWASP 的建議是套用 least privilege。工程師不應該預設擁有整個 secrets store 的讀取權限,application 也不應該拿到超出工作範圍的 credential。
3. Audit log
Audit log(稽核紀錄)是記錄誰在什麼時間,以什麼身分對系統做了什麼事。對 secrets manager 來說,如果只知道 這個值存在,卻不知道 誰讀過這個值,發生事件時仍然很難調查。
Secrets manager 至少應該記錄:
- 誰請求 secret
- 哪個 application 或 machine identity 請求
- 從哪個 project 和 environment 請求
- 請求是否被允許
- 什麼時間讀取
- 誰更新或刪除了 secret
- 誰修改了 access policy
- 是否有人嘗試使用已過期的 secret
理想上,audit log 也不應該和 secret 放在同一個權限邊界裡,避免攻擊者入侵後可以同時刪除資料和清除痕跡。
4. Rotation、expiration 和 revocation
一組 API key 建立後放著不動,通常會一直有效到有人手動撤銷。這種 key 的問題是:使用時間越長,可能散落在越多地方,也越難知道它到底被誰使用。
Secret 至少應該有以下生命週期:
Creation -> Distribution -> Usage -> Rotation -> Revocation -> Expiration其中幾個概念很容易混淆:
- Rotation:建立新的 credential,讓 application 從舊值切換到新值。
- Revocation:讓某個 credential 立刻失效。
- Expiration:在預定時間後讓 credential 失效。
如果 rotation 只能靠人工複製貼上,最後很可能因為怕影響 production 而一直不做。OWASP 建議盡可能自動化 rotation,並在 application 端設計好切換期間的 retry 和相容邏輯。
另一個重要做法是不要讓所有服務共用同一組 key:
錯誤:
所有 project -> 同一組 `OPENAI_API_KEY`
較好的做法:
project-a -> project-a-openai-key
project-b -> project-b-openai-key當 project-a 的 key 被洩漏時,只需要撤銷那一組,不會讓所有服務一起中斷。
5. Secret injection
Secret injection(秘密注入)是指在 application 執行或部署時,才把 secret 提供給 application,而不是把它寫進 source code 或 Docker image。比較理想的架構,是由 application 或 deployment pipeline 使用受到限制的 machine identity(機器身分,也就是代表 application 或 CI/CD job 的非人員身分),向 secrets manager 請求自己需要的資料:
CI/CD pipeline
↓ workload identity
Secrets manager
↓ 只取得指定 environment 的 secrets
Deployment runtime
↓ 注入 application
Application這比把一組可以讀取所有 secret 的管理員 token 放進 CI/CD 設定中好很多。
不過要注意,secret 最後仍然會在 application 執行過程中以某種形式存在。Secrets manager 能夠縮小暴露範圍,但無法讓一個已經被攻陷的 application 讀不到它本來就有權限使用的 secret。
Cloudflare Secrets Store 整合 Workers
以 Cloudflare 為例,除了直接用 wrangler secret put 管理單一 Worker 的 secret,Cloudflare 也提供 Secrets Store。它可以在 account 層級集中保存 secret,再透過 binding(把外部資源掛到 Worker 的設定)將指定的 secret 提供給 Cloudflare Workers 使用。
Cloudflare 目前官方文件列出的 Secrets Store 整合對象是 Cloudflare Workers 和 AI Gateway。它不是讓任意外部主機直接以 SDK 連線的通用 remote .env service。若你的 application 部署在 VPS、Docker、Kubernetes 或其他 self-hosted 環境,就不能直接使用 Workers 的 env.<binding>.get()。
這種做法和把一組 Cloudflare API token 放進 Worker 的環境變數裡不太一樣。Worker 只需要被授予指定 secret 的 binding,不需要在程式碼或 wrangler.jsonc 裡保存可以操作 Cloudflare API 的管理員 token。
建立 account secret
首先在 Cloudflare account 建立 Secrets Store,接著使用 Wrangler 建立 secret。下面的 <STORE_ID> 要替換成 Cloudflare account 中實際的 store ID:
npx wrangler secrets-store secret create <STORE_ID> \\
--name OPENAI_API_KEY \\
--scopes workers \\
--remote執行指令後,Wrangler 會要求你輸入 secret value。Cloudflare 官方文件指出,secret 建立後就不再提供查看原始值的功能;這也代表你需要在外部保留好原始 credential 的 recovery 或 rotation 流程。
建立 secret 需要相應的 Secrets Store 權限,而 secret 也要設定 workers scope,之後才能綁定到 Worker。不要直接把 <STORE_ID> 或 secret value 寫進 repository。
在 Worker 設定 binding
接著在 wrangler.jsonc 中加入 Secrets Store binding:
{
"name": "api-worker",
"main": "./src/index.ts",
"compatibility_date": "2026-08-30",
"secrets_store_secrets": [
{
"binding": "OPENAI_API_KEY",
"store_id": "<STORE_ID>",
"secret_name": "OPENAI_API_KEY"
}
]
}- 這裡的
binding是 Worker 程式中使用的變數名稱 store_id指向 Secrets Storesecret_name則是剛剛建立的 account secret 名稱。
要注意,部署具有 Secrets Store binding 的 Worker,需要 Cloudflare account 中的 Super Administrator 或 Secrets Store Deployer role。這和「可以在自己的程式裡使用 secret」是兩件事:管理 binding 的人需要較高的部署權限,而 Worker 執行時只應該取得必要的 secret。
在程式中讀取 secret
Secrets Store binding 會出現在 Worker handler 的 env 物件上,而且需要非同步呼叫 get() 取得內容:
interface Env {
OPENAI_API_KEY: {
get(): Promise<string>;
};
}
export default {
async fetch(_request: Request, env: Env): Promise<Response> {
const apiKey = await env.OPENAI_API_KEY.get();
const response = await fetch("https://api.openai.com/v1/models", {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
return new Response(await response.text(), {
status: response.status,
headers: {
"Content-Type": "application/json",
},
});
},
};這個範例只是示範取得 secret 和呼叫第三方 API 的位置。實際專案中仍然要避免把 apiKey 印到 log、回傳給 client,或在錯誤訊息中帶出完整內容。也應該限制 OpenAI key 本身的權限與用量,並在第三方服務端保留可以追蹤和撤銷的設定。
Cloudflare 的這個整合方式有幾個值得注意的地方:
- secret 在 account 層級管理,但只會被明確綁定的 Worker 使用。
- binding 設定的是 secret 的參照,不是把 secret value 寫進
wrangler.jsonc。 - Worker 程式透過
env.<binding>.get()取得值,而不是直接把 credential 寫在 source code。 - Secrets Store 的 permission scope 和 Worker binding 權限需要分開檢查。
它可以減少 secret 散落在 repository、CI/CD 設定和不同部署專案中的情況,但 Worker 執行時仍然可以取得自己被授予的明文 secret。如果 Worker 本身已經被攻陷,攻擊者仍可能濫用該 Worker 的權限。因此,Cloudflare Secrets Store 不是把風險消除,而是讓 secret 的儲存、授權和分發邊界更清楚。
使用專門服務後,真的會比較安全嗎?
答案不是單純的「會」或「不會」。把環境變數從部署平台搬到另一個 secrets manager,實際上是重新分配信任邊界:
原本:
Application
↓
Deployment platform environment variables
↓
Deployment platform database
搬遷後:
Application
↓
Secrets manager
↓
Secrets manager database新的服務可能提供更好的:
- 細緻權限
- audit log
- secret versioning
- rotation
- machine identity
- dynamic credentials
但它也會成為新的高價值目標:
- 管理員帳號可能被盜
- service token 可能被洩漏
- 備份或 export 可能暴露 secret
- CI/CD integration 可能有過大權限
- 服務商的內部人員或控制平面可能成為攻擊路徑
- 服務中斷時 application 可能無法取得必要設定
因此不能只看產品是否宣稱「encrypted at rest」。真正要問的是:
誰可以在什麼情況下取得解密後的 secret?
資料庫加密是必要條件,但如果攻擊者已經拿到可以解密資料的內部管理員 credential,單純的 at-rest encryption 並不能阻止資料被讀取。
其實回頭來看 Zeabur 的狀況比較偏向這部分,駭客起初透過洩漏的 AWS 權限來獲取主要 DB 資料,Zeabur 的專案環境變數設定所扮演的角色就是他們自己服務的 secret manager
Cloud provider 的 managed secret manager
例如:
- AWS Secrets Manager
- AWS Systems Manager Parameter Store
- Azure Key Vault
- Google Secret Manager
這類服務適合已經深度使用 AWS、Azure 或 GCP 的團隊,因為可以直接整合 cloud IAM、service identity、network policy 和 audit log。
優點是 application 不一定需要另外保存長期 credential;缺點是權限模型通常比較複雜,跨 cloud 和本地開發的體驗也不一定一致。
通用型 secrets management platform
例如:
- HashiCorp Vault
- Infisical
- Doppler
- 1Password Secrets Automation
這類工具通常會提供 project、environment、CLI、SDK、CI/CD integration、access policy 和 audit log。部分工具也支援 dynamic secrets、certificate management 或 self-hosting。
以 Infisical 官方文件為例,它目前將產品定位在 secrets、certificates 和 privileged access management,並提供 secrets rotation、dynamic credentials、access approval 和 audit 等能力。
但產品功能多,不代表不需要理解權限模型。最重要的仍然是確認 production secret 是否能和 development 隔離,以及 application 的 machine identity 是否只拿到必要資料。
Developer-first 的 environment management
另一類工具比較接近「把 .env 管理做好」,通常強調:
- 本地開發同步
- CLI
- 多環境管理
- 團隊協作
- CI/CD 注入
對小型團隊來說,這可以大幅減少手動複製 .env 的情況。不過在選擇前,仍然要確認它是否提供 production 所需的細緻權限、audit log、rotation、machine identity 和事件處理能力。
能夠同步 .env,不代表它就等同於 enterprise secrets manager。
小型專案可以怎麼做?
不是每個 side project 一開始都需要部署完整的 Vault。重要的是先建立正確的分層和習慣。
本地開發
.env.local
├── 不進 Git
├── 使用 .gitignore
└── 只放本地需要的 credentialRepository 裡可以放不含敏感值的範例:
# .env.example
DATABASE_URL=
OPENAI_API_KEY=CI/CD
使用 CI/CD 平台提供的 encrypted secrets,並且限制到特定 repository、environment 或 job。不要把整組 production secret 設定成所有 pipeline 都能讀取的全域變數。
Production
如果已經使用 AWS、Azure 或 GCP,可以優先評估該 cloud provider 的 managed secret manager,搭配 application identity 取得 secret。
如果專案需要跨 cloud、本地部署、團隊環境同步或 self-hosting,再評估 Vault、Infisical 或其他通用型工具。
事件發生後,應該怎麼處理?
只要有理由認為 secret 可能被讀取,就不能只刪掉部署平台上的那筆資料。正確做法應該是到原本簽發 credential 的服務商進行 revoke 或 rotate。
建議依照以下順序處理:
- 列出所有可能受影響的 project、environment 和 service。
- 依照 credential 類型撤銷或替換 API key、database password、SSH key 和 signing secret。
- 檢查第三方服務的 usage、billing、login 和 audit log。
- 檢查資料庫、server 和 cloud resource 是否有異常連線。
- 將 production 與 staging credential 分開。
- 確認新的 credential 只授予必要權限。
- 保留事件時間線和處理紀錄。
- 檢查 secret 是否曾經出現在 log、image、cache 或備份中。
Zeabur 官方在事件說明中也建議受影響的使用者立即 revoke 和 replace 相關 credential,並檢查第三方服務是否有異常存取、用量或費用。

還記得自己在 8/28 中午先收到 OpenAI 提醒有兩組 API Key 遭濫用才注意到,結果洩漏的兩組 key 都是 2023 年使用的(並部署在 zeabur 上),起初還在想是哪裡洩漏的結果後來是看到 Thread 上有人講才知道,後來到目前為止都還沒收到 Zeabur 官方信件說自己有 key 洩漏...
不過自己後續也應該清理並加以管理這些未使用的 API Key,並設定使用期限才對


不過因為自己有設定運用範圍跟使用上限,所以第一時間也很感謝 OpenAI 幫忙做限流,才得以避免近一步的影響
Written by: Chia1104 CC BY-NC-SA 4.0