iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Security

《30 天為金融法務部門打造 LLM 個資防護閘:去識別化、可控還原與紅隊驗證》系列 第 12

Day 12|Token Vault:整個架構風險最集中的一點

  • 分享至 

  • xImage
  •  

先確認 Vault 裡到底有什麼

昨天說 FPE 是無狀態的,不需要 Vault。那還需要 Vault 的是哪些?

一、自由文字欄位的對應。 人名、地址、公司名——這些用查表替換,必須存對應關係。

二、跨文件一致性所需的索引。 就算用 FPE,你也可能需要知道「TOKEN_X 出現在哪些案件裡」來做 D17 的跨文件查核。

三、稽核所需的最小資訊。 哪份文件在什麼時候被去識別化、用了哪個金鑰版本、產生了哪些 Token。

其中第一項是真正敏感的——它是一張明文對應表

所以第一個設計原則是:能不進 Vault 的就不要進。 每一個可以改用 FPE 的欄位,都應該改用 FPE。Vault 越小越好。

Vault 的資料模型

CREATE TABLE token_map (
    token_id        BYTEA PRIMARY KEY,      -- HMAC(原值) 作為查詢鍵
    token_value     TEXT NOT NULL,          -- 假名/替代值(非敏感)
    encrypted_value BYTEA NOT NULL,         -- 加密後的原值
    key_version     TEXT NOT NULL,          -- 用哪個 DEK 版本加密
    entity_type     TEXT NOT NULL,          -- PERSON / ADDRESS / ORG
    scope_id        TEXT NOT NULL,          -- 案件/session 範圍(見 D18)
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at      TIMESTAMPTZ
);

CREATE INDEX idx_token_lookup ON token_map (token_value, scope_id);
CREATE INDEX idx_expiry ON token_map (expires_at) WHERE expires_at IS NOT NULL;

幾個設計重點:

token_id 用 HMAC 而不是原值。 這樣可以做「這個原值之前是否已經有 Token」的查詢,而不需要在索引裡存明文。

encrypted_value 而不是明文。 就算整張表被 dump 出去,沒有金鑰也還原不了。這是最後一道防線。

key_version 必須記錄。 金鑰輪替後,舊 Token 要用舊版金鑰解。沒記這個欄位,輪替就等於資料遺失。

scope_id 從一開始就要有。 這是 D18 Token 隔離的基礎。事後要加這個欄位極其痛苦,因為既有資料無法回溯判斷 scope。

expires_at 讓 Vault 會自己縮小。 案件結案後 Token 應該過期,過期後資料被清除。這是唯一能阻止 Vault 無限成長的機制。

五道防護

第一道:儲存加密。 資料庫層級的加密(Cloud SQL CMEK / TDE),加上欄位層級的加密(encrypted_value)。兩層都要,因為它們防的是不同的攻擊——前者防磁碟被拿走,後者防資料庫被查詢。

第二道:網路隔離。 Vault 不應該有公開端點。放在私有子網、用 Private Service Connect、加上 VPC Service Controls 邊界。

gcloud access-context-manager perimeters create pii_vault_perimeter \
  --title="PII Vault Perimeter" \
  --resources=projects/VAULT_PROJECT_NUMBER \
  --restricted-services=sqladmin.googleapis.com,cloudkms.googleapis.com \
  --policy=POLICY_ID

第三道:身分隔離。 這一道是最容易被做錯的。列一下權限矩陣:

身分 寫入 Vault 讀取 token_value 解密 encrypted_value
去識別化服務
應用服務
還原服務
DBA

去識別化服務只寫不讀——它產生 Token 之後不需要再查。還原服務只讀不寫。應用服務兩者都不行,它只能呼叫還原服務的 API。

DBA 全部打叉,靠的是 CMEK:DBA 有資料庫管理權限,但沒有 KMS 解密權限,所以他能看到 encrypted_value 這個欄位存在,但看不到內容。

第四道:存取記錄。 每一次對 Vault 的查詢都要記錄。不只是「還原 API 被呼叫了」,而是「哪個 token_id 被查了」。這樣才能偵測 D3 提到的批次還原濫用。

第五道:備份保護。 這是最常出事的一道。備份必須加密、備份的存取權限要跟本體同級、備份不能存在權限較鬆的儲存體。

我看過的實際案例:Vault 本體防護做得很好,但每日備份用 pg_dump 導出到一台檔案伺服器,那台伺服器的權限是整個 IT 部門都能讀。整個架構的安全等級等於最弱那一環。

金鑰輪替:一個要提前規劃的維運事件

DEK 輪替時,既有的 encrypted_value 需要用新金鑰重新加密。作法是漸進式:

def rotate_batch(batch_size=1000):
    rows = db.query(
        "SELECT token_id, encrypted_value, key_version "
        "FROM token_map WHERE key_version != %s LIMIT %s",
        (CURRENT_KEY_VERSION, batch_size),
    )
    for row in rows:
        plaintext = kms_decrypt(row.encrypted_value, row.key_version)
        new_cipher = kms_encrypt(plaintext, CURRENT_KEY_VERSION)
        db.execute(
            "UPDATE token_map SET encrypted_value=%s, key_version=%s "
            "WHERE token_id=%s",
            (new_cipher, CURRENT_KEY_VERSION, row.token_id),
        )
        del plaintext   # 明文在記憶體中的存在時間最小化

輪替期間 plaintext 會短暫存在於記憶體。這個過程應該:

  • 在隔離的環境執行,不與一般服務共用主機
  • 不寫任何日誌(除了進度計數)
  • 有獨立的服務帳號,用完即撤銷權限

Vault 的生命週期

最後一個容易被忽略的問題:這些資料要留多久?

法務案件結案後,Token 對應還需要嗎?多數情況下不需要。但如果沒有明確的清除機制,Vault 會一直長大,風險一直累積。

建議的政策:

狀態 保留期 動作
案件進行中 不限 保留
結案後 依案件類型(例如 1–3 年) 保留
保留期滿 刪除對應,但保留稽核紀錄

注意最後一列:刪除 Token 對應,但保留「曾經存在過這個 Token」的稽核紀錄。這樣既縮小了洩漏面,又保留了事後查核的能力。

自動清除的實作:

DELETE FROM token_map
WHERE expires_at IS NOT NULL AND expires_at < now();

排程執行,並且把刪除筆數寫進稽核日誌。清除本身也是一個需要被稽核的動作——因為惡意的大量刪除也是一種攻擊(破壞可用性與可稽核性)。


明天換個角度,看地端自建的作法。


關於作者

我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend

有想討論的架構細節或不同意見,留言或私訊都歡迎。


上一篇
Day 11|實作:SDP + Cloud KMS 的可逆 Token 化
下一篇
Day 13|地端自建:同型改寫式遮罩的設計
系列文
《30 天為金融法務部門打造 LLM 個資防護閘:去識別化、可控還原與紅隊驗證》14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言