---
title: "Secret Management：什麼東西算 secret？怎麼管理洩漏風險？"
description: "洩漏風險 = 副本數 × 權限範圍 × 壽命"
lang: "zh-Hant-TW"
tags: ["security", "secret-management", "cloud", "DevOps"]
pubDate: 2026-07-01T00:00:00.000Z
updatedDate: 2026-07-03T00:00:00.000Z
---

## 什麼算 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_`              | 用指定 Stripe 帳戶收付款                                | **是**                                           | **secret**       |
| DB 密碼                        | 讀寫整個資料庫                                          | **是**                                           | **secret**       |
| JWT signing key                | 偽造任何使用者的 token                                  | **是**                                           | **secret**       |
| OAuth `client_secret`          | 冒充指定 app 取得授權                                   | **是**                                           | **secret**       |
| Webhook signing secret         | 偽造 webhook 觸發業務邏輯                               | **是**                                           | **secret**       |
| DB hostname                    | 知道資料庫位置                                          | 否，密碼和防火牆在擋                             | sensitive config |
| 含 stack trace 的 error        | 知道內部框架版本和檔案結構                              | 否，但降低攻擊成本                               | sensitive config |
| `NODE_ENV`、public URL         | 無                                                      | —                                                | public 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](https://firebase.google.com/docs/rules)，不靠 key 保密。
- **Sentry DSN**：讓 client-side SDK 回報 error。[Vendor 預期公開](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)，靠 rate limit + inbound filter 擋濫用。
- **Stripe** `pk_live_`：建立 payment intent。完成收款需要 server-side `sk_live_` 驗證，pk 不是防線。
- **OAuth** [`client_id`](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3)：識別 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 裡仍有副本。
- **Log**：`console.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](https://blog.huli.tw/2026/05/25/dive-into-npm-supply-chain-attack/)，Coupang 的個資外洩事件則是 signing key 曾 hardcode 在開發者筆電上，[前員工帶走後造成的](https://blog.huli.tw/2026/03/19/coupang-insider-kms-and-jwt/)。
- **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 secrets`、`aws 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，影響範圍遠超預期](https://blog.huli.tw/2021/08/22/cdnjs-and-supply-chain-attack/)。把 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](https://nhimg.org/glossary/credential-exposure-window/)。縮小任何一個都在降低風險，但效果不等，壽命從永久變成 15 分鐘的效果，通常比副本從 10 份減到 5 份大得多。後面的策略就是照這三個變數來組織的。

## Secret 管理策略

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

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

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

```ts
const KEY = "sk-...";
```

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

設定檔寫在 git 裡（`apphosting.yaml` 寫值、`docker-compose.yml` 寫死）也是同一個問題，從 git 的角度看沒有差別，值都在 history 裡。

核心問題：**副本數 = clone 數，而且永遠拿不回來**。

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

```bash
# .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](https://cloud.google.com/secret-manager/docs)、AWS [Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)、[HashiCorp Vault](https://developer.hashicorp.com/vault)、[Doppler](https://www.doppler.com/)。

跟平台 env var 的差別不在加密（平台 env var 通常也 encrypted at rest），而在三件事：

|                  | 平台 env var     | Secret Manager                                                                                                      |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| **存取控制粒度** | 平台帳號層級     | per-secret、per-service-account 的 [IAM](https://cloud.google.com/iam/docs/overview)                                |
| **Audit log**    | 弱或無           | 完整：誰在什麼時候讀了哪個 secret（[Cloud Audit Logs](https://cloud.google.com/secret-manager/docs/audit-logging)） |
| **版本化**       | 無（改錯回不去） | 多版本，可以秒退到前一版                                                                                            |

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 為例

```bash
# 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"
```

```yaml
# 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 的做法，見[後面的「特定場景」](#延伸閱讀特定場景不需要持有-credential)。

## 維護與應變

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

### 什麼時候該 rotate

這裡講的是 static credential，也就是 Stripe key、DB 密碼這類沒辦法自動過期的東西。（平台身分機制發出的短效 token 壽命本來就是分鐘級，不需要額外操心 rotation，見[後面的「特定場景」](#延伸閱讀特定場景不需要持有-credential)。）

**防線到位的情況下，出事再 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 如 [gitleaks](https://github.com/gitleaks/gitleaks)、[trufflehog](https://github.com/trufflesecurity/trufflehog)。跟任何第三方工具一樣，導入前自己評估風險
- **Platform-level**：GitHub [Push Protection](https://docs.github.com/en/code-security/secret-scanning/push-protection-for-repositories-and-organizations)，擋已知格式的 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`](https://github.com/newren/git-filter-repo)（Git 官方推薦的 history rewrite 工具）改寫再 force push，CI log 做 purge，image registry 清掉舊版。

## 延伸閱讀：secret 沒管好會怎樣

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

- **[從 Coupang 的個資外洩談內部威脅、金鑰管理與 JWT](https://blog.huli.tw/2026/03/19/coupang-insider-kms-and-jwt/)** — 前 Coupang 員工帶走了 hardcode 在筆電上的 JWT signing key，造成個資外洩。文章從這個事件出發，討論 insider threat、key management lifecycle（從產生到銷毀）、Secret Manager 和 KMS 的差別。對應這篇的「副本在開發者裝置」和「KMS」。
- **[從 cdnjs 的漏洞來看前端的供應鏈攻擊與防禦](https://blog.huli.tw/2021/08/22/cdnjs-and-supply-chain-attack/)** — cdnjs 的 RCE 漏洞讓攻擊者讀到 process 的環境變數，拿到有 write access 的 GitHub API key。示範了「服務被打穿 → env var 裡的 secret 全部曝光」的真實後果，以及 scope 太大時 blast radius 有多嚴重。對應這篇的「範圍」和「blast radius」。
- **[npm 供應鏈攻擊從頭談起：原理、手法與防禦方式](https://blog.huli.tw/2026/05/25/dive-into-npm-supply-chain-attack/)** — 惡意 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](https://cloud.google.com/compute/docs/metadata/overview)（只從 instance 內部網路可達）要一個短效 token。平台透過基礎設施層面就知道「這個 request 來自哪個 instance」，不需要服務出示任何 credential，就像 OS 知道哪個 user 在跑哪個 process，process 不需要輸入密碼。

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

- 沒有 static credential 存在任何地方，不在 git、不在 `.env`、不在 Secret Manager
- 身分驗證來自「服務跑在這個平台上」這個事實，不是來自服務持有的 secret
- 對方驗證 token 用的是平台的 [public key](https://datatracker.ietf.org/doc/html/rfc7517)（公開的，不是 secret）

GCP 叫 [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation)，AWS 叫 [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)（EKS）或 [instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html)（EC2）。

常見場景：

- **Cloud Run 服務互打**：SA attached → 背景自動取短效 ID token → 對方驗簽
- **GitHub Actions → GCP / AWS**：[OIDC provider trust](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-openid-connect-to-access-cloud-resources) → 不需要存 SA key JSON。GitHub 能識別 workflow 的身分（因為 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](https://cloud.google.com/kms/docs) / [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html) 的做法不一樣。Key 在平台管理的模組裡（[HSM 等級](https://cloud.google.com/kms/docs/hsm)是專用硬體，[SOFTWARE 等級](https://cloud.google.com/kms/docs/algorithms#protection-levels)是 Google 基礎設施內的軟體模組），[無法取得明文 key](https://cloud.google.com/kms/docs/faq)。服務不持有 key，而是呼叫 KMS API：「請用這把 key 簽這份資料」「請解這段密文」「請算這段 HMAC」，crypto 操作在 KMS 端完成，key 不會進到服務的 process memory。

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

即使服務被打穿、memory 被 dump，攻擊者拿不到 key 本身，只能在服務還在跑的期間濫用 KMS API。此時可以立刻 [disable 那個 key version](https://cloud.google.com/kms/docs/enable-disable) 來止血。注意 crypto 操作的 audit log（[Data Access log](https://cloud.google.com/kms/docs/audit-logging)）需要手動啟用，不是預設開的，上線前要確認有開，否則出事時沒有「誰在什麼時候用了這把 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。
