Secret 管理:什麼算 secret、怎麼想洩漏風險、從 .env 到 Secret Manager

Published on Updated on Written by

怎麼判斷一個值該不該保護、怎麼想洩漏風險、從 .env 到 Secret Manager 該怎麼選,以及出事時怎麼止血。

什麼算 secret?

不是每個 env var 或 key 都需要同等對待。判斷一個值需不需要保護、需要多少保護,問兩件事:

  1. 這個值被濫用,能造成什麼損害?(門後面有什麼值得守的)
  2. 這個值的保密,是不是阻止那個損害的唯一防線?(有沒有其他安全機制在擋)

根據這兩個問題,分成三類:

Secret:有損害,而且保密是唯一防線

Stripe sk_live_ 保密,保護的是「只有你能用你的帳戶收款」——沒有其他機制在擋,外洩就是直接的財務損害。DB 密碼保密,保護的是「只有授權的人能連進資料庫」——密碼就是最後一道門。JWT signing key 保密,保護的是「只有 server 能發出合法 token」。這類值外洩後唯一的止損手段是 revoke 再換一把新的。

Sensitive config:有損害,但保密不是唯一防線

DB hostname 公開了,密碼和防火牆還在擋——但攻擊者省去了探測步驟,其他防線承受的壓力更大。Internal API endpoint、含 stack trace 的 error message 也屬於這類。分類上它不是 secret,但這篇後面討論的風險思考和管理策略大部分也適用——差別在優先順序和出事時的緊急程度,不在「要不要管」。

注意 sensitive config 的風險會堆疊:攻擊者同時拿到 DB hostname + DB username + 內部 API 結構 + error format 裡的框架版本——每個單獨看都只是 sensitive config,但組合起來等於幫攻擊者省去大量探測工作,讓剩下的防線(密碼)承受的壓力大很多。實務上,工具只給你兩個選項時(例如 GitHub Actions 的 vars vs secrets),sensitive config 選保護那邊——多保護的成本幾乎是零,少保護的後果不對稱。

Public config:沒有值得守的損害

NODE_ENV、public URL、feature flag。不在這篇範圍。同理,一把 test sandbox API key 如果 sandbox 裡沒有真實資料、也不會產生費用,即使 key 保密是唯一防線,也不需要用 production secret 的力氣去保護。

範例:用這兩個問題判斷常見的值

① 被濫用能造成什麼損害?② 保密是唯一防線?判定
Stripe sk_live_用你的帳戶收付款secret
DB 密碼讀寫整個資料庫secret
JWT signing key偽造任何使用者的 tokensecret
OAuth client_secret冒充你的 app 取得授權secret
Webhook signing secret偽造 webhook 觸發業務邏輯secret
DB hostname知道資料庫位置否——密碼和防火牆在擋sensitive config
含 stack trace 的 error知道內部框架版本和檔案結構否——但降低攻擊成本sensitive config
NODE_ENV、public URLpublic config
Internal API endpoint直接呼叫 internal API否——但如果 API 沒 auth,保密變成事實上的唯一防線看情境
Feature flag通常無——但如果控制付費牆或 admin 功能,保密就是唯一防線看 flag 控制什麼看情境
Test sandbox key(空 sandbox)無實際損害是,但門後面沒東西看損害規模

有些值名字帶 key 或 secret,但在進到上面兩個問題之前,就已經被架構決定是 public——它們結構上必須出現在 client-side code 裡才能運作,vendor 知道不可能保密,所以刻意把安全性建立在其他機制上:

  • Firebase apiKey:識別 project、呼叫 public API。安全性靠 security rules,不靠 key 保密。
  • Sentry DSN:讓 client-side SDK 回報 error。Vendor 預期公開,靠 rate limit + inbound filter 擋濫用。
  • Stripe pk_live_:建立 payment intent。完成收款需要 server-side sk_live_ 驗證——pk 不是防線。
  • OAuth client_id:識別 app 身分。授權靠 client_secret + auth flow,不靠 client_id 保密。
Note

主流前端框架怎麼處理這條界線

NEXT_PUBLIC_*(Next.js)、VITE_*(Vite)、REACT_APP_*(CRA)——有這些 prefix 的環境變數會在 build 時被靜態替換進 client bundle,沒有的則只存在於 server-side。Secret 和 sensitive config 都不該放在這些 prefix 底下。

怎麼想 secret 的風險?

Secret 以明文存在的每個地方,都是一個獨立的洩漏口。一把 API key 如果同時在 git history、三台開發者筆電、CI log 裡各有一份——你要守的不是一個 secret,是四個洩漏口,任何一個被打開就算洩漏。

管理 secret 就是控制三個變數:副本數每份副本的權限範圍副本的壽命

變數問的是改善方向
副本數明文存在幾份、在哪越少越好
權限範圍每份副本能碰什麼越窄越好
壽命每份副本活多久越短越好(槓桿最大)

副本:存在幾份,在哪

每一份明文副本都是獨立的洩漏口。副本產生的常見途徑:

  • Git history:值 commit 進 repo 的那一刻,每一份 clone 都帶著它。下一個 commit 改掉也沒用——git 設計上就是永遠保留歷史。repo 有幾個 collaborator,就至少有幾份 clone。「repo 是私有的所以沒關係」是常見的誤判——private 只擋公開掃描,不改變副本數量。
  • Build artifact:Build-time 注入的 env var 會被烤進 client bundle、Docker image、prerendered HTML。從 source 拿掉沒用——image registry 和 build cache 裡仍有副本。
  • Logconsole.log(process.env.STRIPE_KEY) 會把值印進 Cloud Run stdout、CI build log、APM dashboard。不只是你自己寫的 log——third-party SDK 的 debug mode、LLM provider 的 prompt transcript 也可能截到。
  • 開發者裝置.env 在每位開發者的筆電上各一份。筆電遺失、離職未清、裝置被入侵——每台都是獨立風險。而且不只是物理風險:惡意 npm package 的 postinstall script 可以直接讀走機器上的 env var,Coupang 的個資外洩事件則是 signing key 曾 hardcode 在開發者筆電上,前員工帶走後造成的
  • AI 工具的 context:用 Cursor、Copilot、或其他 AI 工具開發時,.env 的內容可能被讀進 prompt context、出現在 AI provider 的 log 裡、甚至被 agent 寫進 commit message。非工程師用 AI 改 code 時更容易發生——他們不會注意 .gitignore 的意義。
Caution

操作 secret 時不要讓 agent 碰到值

盤點、輪替、migration 都涉及讀到實際的 secret 值。讓 LLM 看到那串值本身就是一次洩漏——值會進 provider 的 API log、可能在 cache 或 transcript 中殘留、agent 也可能在後續 commit 或 summary 裡把值吐出來。Secret 操作用確定性的本機工具:secret scanner 跑 scan、git filter-repo(Git 官方推薦)改 history、provider CLI(gcloud secretsaws secretsmanager)做 rotation。Agent 可以幫忙寫 migration script,但實際的 secret 值不該進 agent context

而且不是每份副本風險一樣大——同一份 .env,在有 disk encryption 和 MDM 的公司筆電上,跟在沒設密碼的私人筆電上,被拿走的難度差很多。副本在 CI log 裡(可能全 team 都看得到、retention 不確定)跟在有 IAM 的 Secret Manager 裡,曝光面也完全不同。數副本不只是數「幾份」,也要看每份落在什麼樣的環境。

副本越少、每份所在的環境越受控,要守的口越少。理想狀態是 secret 只存在於一個有存取控制的地方(Secret Manager),runtime 需要時取用,不落地在任何人的裝置或任何 artifact 裡。

範圍:每份副本能做什麼

同樣是洩漏一把 credential,損害可以差非常多:

  • 一把能讀寫整個 GCP project 的 SA key → 資料庫、storage、所有 secret 全部曝光
  • 一把只能讀某個 Firestore collection 的 SA key → 損害限於那個 collection

這就是 blast radius——出事時的受災範圍。範圍越窄,損害越小、止血越快。

範圍由幾件事決定:

  • 權限:這把 credential 能做什麼操作?read-only 跟 admin 差很多。
  • 資源:能碰哪些資源?per-service 的 credential 比「整個 project 共用一把」安全得多。
  • 環境:能進哪些環境?staging 的 credential 進不了 prod,洩漏了損害小很多。

常見問題是起步時一把 SA key 所有服務共用——設定簡單,但一旦洩漏就是整個 project 見光。cdnjs 的 RCE 漏洞就是一個例子:攻擊者讀到 /proc/self/environ拿到的 GitHub API key 恰好有 write access,影響範圍遠超預期。把 credential 拆成 per-service、per-environment,是讓 blast radius 從「全毀」變成「局部受損」的最直接做法。

壽命:副本活多久

Static credential(API key、DB 密碼)發出來就永遠有效,直到你手動 revoke。如果洩漏了但你沒發現——很常見,因為攻擊者用的就是合法 credential,access log 看起來跟正常請求一樣——它就永遠可用。

Short-lived credential(Workload Identity 換來的 token、OAuth access token)有 TTL,過期自動失效。即使洩漏,攻擊者的可用時間被壓縮到分鐘或小時等級。

壽命是三個變數裡槓桿效果最大的。把壽命從「永久」壓到「15 分鐘」,等於不管你有沒有偵測到洩漏,損害都自動被限制在那 15 分鐘內能造成的範圍。


這三個變數的乘積大致決定了風險的大小:

risk ∝ 副本數 × 權限範圍 × 壽命

業界把這個概念叫做 credential exposure window。縮小任何一個都在降低風險,但效果不等——壽命從永久變成 15 分鐘通常比副本從 10 份減到 5 份的改善大得多。後面的策略就是照這三個變數來組織的。

Secret 管理策略

上面三個變數對應不同的管理做法。以下從不安全到安全排列,每一步改善的都是副本數或存取控制粒度:

做法改善什麼沒改善什麼
.env副本不隨 clone 傳播副本散在各開發者筆電,環境防護程度不一
平台 env var副本集中到受管理的平台存取控制粗、audit 弱
Secret Managerper-secret IAM + audit logstatic credential 仍永久有效

寫在原始碼裡 — 最危險的起點

const KEY = "sk-...";

值直接在 code 裡。git push 那一刻進 history,之後每一份 clone 都帶著——即使下一個 commit 改掉,history 裡的值不會消失。

設定檔寫在 git 裡(apphosting.yaml 寫值、docker-compose.yml 寫死)也是同一個問題——從 git 角度看沒有差別,值都在 history 裡。

核心問題:副本數 = clone 數,而且永遠拿不回來

本機 .env — 開發環境的起點

# .env(不進 git)
STRIPE_KEY=sk_test_...

.gitignore 排除,程式用 process.env.X 讀。

改善了什麼:值不在 git history 裡了——副本不再隨 clone 傳播。

沒改善什麼:每位開發者筆電上各有一份,副本數 = 開發者人數。而且每台筆電的防護程度不一——有人有 disk encryption + 螢幕鎖,有人用私人筆電、沒設密碼。rotation 要私下傳新值,onboarding 要找人要 .env,沒有 audit log。

.env 在 3–5 人團隊可以靠信任撐著,但它的前提是:用 sandbox / test key(不是 prod)、.env.example 保持更新。任何一個下列事件發生時就撐不住了——第一個外部合作者進來、有人離職、筆電遺失、客戶安全審查。

平台 env var / CI secret — 小團隊生產環境的起點

Vercel environment variables、Cloud Run env vars、GitHub Actions secrets、GitLab CI variables。Secret 存在平台側(通常 encrypted at rest),deploy 或 CI 時注入。

改善了什麼:

  • 副本不在 git ✅
  • 副本不在開發者筆電 ✅——從防護程度不一的個人裝置,集中到受管理的平台

沒改善什麼:

  • 存取控制是平台帳號層級——帳號被入侵就整組見光
  • 通常沒有 versioning(改錯回不去)
  • Audit log 很弱或沒有

對一個 3–6 人 repo 的團隊,這已經是務實的生產起點。但只要有 long-lived production secret(LLM key、Stripe live key、生產 DB 密碼),值得再往上推一步。

Secret Manager — 生產環境的標準做法

Google Secret Manager、AWS Secrets ManagerHashiCorp VaultDoppler

跟平台 env var 的差別不在加密(平台 env var 通常也 encrypted at rest),而在三件事:

平台 env varSecret Manager
存取控制粒度平台帳號層級per-secret、per-service-account 的 IAM
Audit log弱或無完整——誰在什麼時候讀了哪個 secret(Cloud Audit Logs
版本化無(改錯回不去)多版本,可以秒退到前一版

Secret Manager 的核心價值是把存取控制從「有帳號就能看全部」升級到「只能看被授權的那幾個」。但注意:它不自動幫你設好範圍——如果你給整個 developers group 都授 secretAccessor,效果等同平台 env var。工具給你 scope 的能力,你得自己用。

每個 secret 從平台 env var 搬到 Secret Manager 大約 10–15 分鐘(建 secret + IAM binding + 改 config reference + deploy + 驗證)。多數 codebase 有 5–15 個 secret,半天到一天搞定。

以 GCP Secret Manager + Firebase App Hosting 為例

# 1. 建 secret(只建 metadata)
gcloud secrets create stripe-live-key --replication-policy=automatic
# 2. 加 version(實際值)
echo -n "sk_live_..." | gcloud secrets versions add stripe-live-key --data-file=-
# 3. 授權給 runtime SA
gcloud secrets add-iam-policy-binding stripe-live-key \
--member="serviceAccount:app@PROJECT.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
# 4. apphosting.yaml 只放名字,不放值
env:
- variable: STRIPE_SECRET_KEY
secret: stripe-live-key # ← 名字,不是值
availability: [RUNTIME]

Deploy 後 App Hosting 用 instance 的 SA 呼叫 Secret Manager API,GCP 檢查 IAM binding,通過才回傳明文注入 env var。每次 access 都進 Cloud Audit Logs。

不管存放做得多好,static credential 有一個根本限制:洩漏後它永遠有效,直到你手動 revoke。 而你可能很久才發現洩漏——攻擊者用的是合法 credential,access log 看起來跟正常請求一模一樣。壽命是三個變數裡槓桿最大的,但上面的做法都沒動到壽命——它們改善的是副本數和存取控制。某些特定場景有根本不需要持有 credential 的做法,見後面的「特定場景」

維護與應變

策略設好之後,secret 管理變成日常:偵測洩漏、出事時止血、判斷什麼時候該 rotate。

什麼時候該 rotate

這裡講的是 static credential——Stripe key、DB 密碼這類你沒辦法讓它自動過期的東西。(平台身分機制發出的短效 token 壽命本來就是分鐘級,不需要你操心 rotation——見後面的「特定場景」。)

防線到位的情況下,出事再 rotate 是合理的。 如果 Secret Manager + IAM scope 做好了、偵測有在跑,大部分 secret 在偵測到洩漏、有人離職、或安全審查時才 rotate 就夠。精力花在縮副本和收窄範圍通常比排定 rotation cadence 更值得。

例外是高損害、難偵測的 credential。 有些洩漏很難被發現——攻擊者用的就是合法 credential,access log 看起來跟正常請求一樣。如果一把 credential 的損害很大(production DB、Stripe live key)而且你不確定偵測能抓到所有情境,定期輪替(每季或 90 天)是多一層保險。

偵測洩漏

偵測是多一層防線,不是保證——沒有工具能抓到所有洩漏(自訂格式的 token、binary blob、已經在 history 裡的值都可能漏掉)。

常見的偵測工具和機制:

  • Pre-commit / CI scan:在值進 history 之前攔截。常見的 open-source secret scanner 如 gitleakstrufflehog——跟任何第三方工具一樣,導入前自己評估風險
  • Platform-level:GitHub Push Protection——擋已知格式的 secret push
  • Provider-side:能設 key 過期就設

外洩後怎麼處理

不管從哪條途徑洩漏,處理順序的原則一樣:

  1. 先在 provider 端 revoke,再做其他事。 改檔案、清 history、刪 log 都不會讓已洩出的值失效——只有 provider 端的 revoke 能讓它變廢紙。這一步要最先做,因為清理需要時間,而攻擊者可能正在用。
  2. 找出洩漏原因,修掉它。 在放新 credential 進去之前,先確保同一個洞不會讓新值馬上再漏一次——洩漏途徑沒堵住就放新值,等於馬上再漏一次。原因通常對應這篇前面「副本」段落列的那些途徑(git history、CI log、開發者裝置、build artifact),修法就是把該途徑的副本消滅或移到受控環境。
  3. 原因修掉後,發新的 credential,確認服務已切換。 看 access log 確認新值在用、舊值沒有流量。持續觀察 24–72 小時——若舊值還有流量,代表攻擊者已在用,需要進一步調查影響範圍。
  4. 清理殘留副本(看情境決定優先級)。 舊值已經 revoke,但留著會造成噪音和風險:自動掃描工具和 AI agent 無法判斷這些值是否還有效、密碼可能被重複使用在其他地方、命名 pattern 會被看到。是否值得花力氣清,取決於這些問題的嚴重性。git history 用 git filter-repo(Git 官方推薦的 history rewrite 工具)改寫 + force push、CI log purge、image registry 清舊版。

延伸閱讀:secret 沒管好會怎樣

這篇講的是防禦面——怎麼管好 secret。下面幾篇從攻擊面切入,看沒做好這些防禦時會發生什麼事:

  • 從 Coupang 的個資外洩談內部威脅、金鑰管理與 JWT — 前 Coupang 員工帶走了 hardcode 在筆電上的 JWT signing key,造成個資外洩。文章從這個事件出發,討論 insider threat、key management lifecycle(從產生到銷毀)、Secret Manager 和 KMS 的差別。對應這篇的「副本在開發者裝置」和「KMS」。
  • 從 cdnjs 的漏洞來看前端的供應鏈攻擊與防禦 — cdnjs 的 RCE 漏洞讓攻擊者讀到 process 的環境變數,拿到有 write access 的 GitHub API key。示範了「服務被打穿 → env var 裡的 secret 全部曝光」的真實後果,以及 scope 太大時 blast radius 有多嚴重。對應這篇的「範圍」和「blast radius」。
  • npm 供應鏈攻擊從頭談起:原理、手法與防禦方式 — 惡意 npm package 透過 postinstall script 讀取開發者機器和 CI 環境的 env var、cloud key,再回傳給攻擊者。開發者裝置上的 .env 和 CI 裡的 secret 都是目標。對應這篇的「副本在開發者裝置」——supply chain attack 是副本被偷走的另一條路徑。

延伸閱讀:特定場景不需要持有 credential

前面的做法都假設你必須持有一個 secret value——改善的是怎麼存它。但某些特定場景有根本不同的機制,讓你不需要持有那個值。這些機制各自只適用於特定的問題領域,不是通用的 secret 管理策略。

服務對服務:你在平台基礎設施上,就不需出示 credential

傳統做法:你的服務要打 Cloud SQL,所以你持有一把 SA key(static credential),每次連線時出示。

平台身分機制的做法不一樣。你的服務跑在平台上(GCP、AWS),平台在部署時就指定了它的身分——「這個 Cloud Run instance 是 service account X」。當服務需要存取其他資源時,它去問平台的 metadata server(只從 instance 內部網路可達)要一個短效 token。平台透過基礎設施層面就知道「這個 request 來自我部署的那個 instance」,不需要服務出示任何 credential——就像 OS 知道哪個 user 在跑哪個 process,process 不需要輸入密碼。

拿到的 token 壽命通常是 15 分鐘到 1 小時,過期後服務再問 metadata server 要一個新的。整個過程:

  • 沒有 static credential 存在任何地方——不在 git、不在 .env、不在 Secret Manager
  • 身分驗證來自「你跑在這個平台上」這個事實,不是來自你持有的 secret
  • 對方驗證 token 用的是平台的 public key(公開的,不是 secret)

GCP 叫 Workload Identity Federation,AWS 叫 IRSA(EKS)或 instance profile(EC2)。

常見場景:

  • Cloud Run 服務互打:SA attached → 背景自動取短效 ID token → 對方驗簽
  • GitHub Actions → GCP / AWSOIDC provider trust → 不需要存 SA key JSON。GitHub 知道你是誰(因為 workflow 跑在它上面),GCP 設定了信任 GitHub 的 OIDC provider,所以 GitHub 的 assertion 就夠了
  • Kubernetes pod → Cloud SQL:Workload Identity 把 pod 身分映射成 GCP SA

回到三個變數——副本數 = 0(沒有 static credential)、壽命被壓到分鐘級(token 自動失效)、範圍跟 static credential 一樣由 IAM 決定。

限制:只適用於雙方都在支援平台上的服務對服務場景。你的服務必須跑在平台上,對方也必須認平台簽的 token。

簽章與加解密:KMS 代做運算,不需要持有 key

傳統做法:你的服務要簽 JWT,所以你持有一把 signing key,存在 Secret Manager 或 env var 裡,runtime 讀出來呼叫 crypto library。

Cloud KMS / AWS KMS 的做法不一樣。Key 在受管理的模組裡(HSM 等級是專用硬體,SOFTWARE 等級是 Google 基礎設施內的軟體模組),你永遠拿不到明文。你的服務不持有 key,而是呼叫 KMS API:「請用這把 key 簽這份資料」「請解這段密文」「請算這段 HMAC」——crypto 操作在 KMS 端完成,key 不進你的 process memory。

常見場景:JWT 簽章、資料庫欄位加密、code signing、HMAC 驗證。

即使服務被打穿、memory 被 dump,攻擊者拿不到 key 本體——只能在服務活著的期間濫用 KMS API。你可以立刻 disable 該 key version 止血。注意 crypto 操作的 audit log(Data Access log)需要手動啟用,不是預設開的——上線前要確認有開,否則出事時沒有「誰在什麼時候用了這把 key」的紀錄。

限制:只適用於操作本身是密碼學運算(sign、encrypt、decrypt、MAC)的場景。

延伸主題

  • Secret Manager — 這篇解釋了為什麼值得用 Secret Manager。實際導入時會碰到更多細節:multi-environment 的命名與 IAM 結構、rotation 自動化、跟 CI/CD pipeline 的整合、從平台 env var 搬過來的 migration 步驟。從你用的 cloud platform 官方文件開始:GCP Secret Manager、AWS Secrets Manager、HashiCorp Vault。
  • IAM / least privilege — Secret Manager 的價值在「你可以 scope 得很細」,但 scope 怎麼設是 IAM 的事。這篇反覆提到 per-service、per-environment 的 credential 拆分,實際做的時候需要理解你用的 cloud platform 的 IAM model(role、binding、policy、inheritance)。Secret Manager 沒搭好 IAM 等於平台 env var。從你用的 cloud platform 開始:GCP IAM、AWS IAM。