iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

昨天 Day 17 我們把入境守門的第二件事講完了——八類 PII(個資)怎麼辨識,為什麼「員編 6 位數、Email 裡也夾數字」這件小事會逼出一套講究先後順序的比對設計。Day 16、Day 17 連起來,入境這一關的兩件事(擋注入、遮個資)都交代清楚了。但這兩天有一個我刻意還沒回答的問題:如果這道關被人關掉了呢?守門是可抽換的,設定一改成 portal.guardrail.provider: mock,整道入境檢查就形同虛設。那行員打進來的個資、攻擊者塞進來的注入字串,是不是就一路裸奔到底了?今天就回答這題,答案叫縱深防禦(defense in depth)——不把賭注押在任何單一層上。

本篇結構:

  • 單點防護的脆弱
  • 三道關加一道保底:把防線疊起來
  • 重複處理的成本,換來「不把合規寄託在上游」

單點防護的脆弱

先把問題講白。前兩天描述的入境守門,聽起來像一道堅固的城門:注入擋掉、個資遮掉,乾乾淨淨才放行。但這道城門有兩個我們無法假裝不存在的弱點:

弱點 是什麼 為什麼躲不掉
可以被合法地關掉 「守門開著」不是系統不變量(invariant),只是一個當下的設定狀態 守門是可抽換的零件,本來就留了合法的關閉與替換入口
它可能漏 規則列舉得再全,總有沒涵蓋到的格式 列舉式的句式比對,碰到沒見過的變形就會漏

第一個弱點不是假設,是 Day 14、Day 15 親手做出來的能力:逐請求能用 header 覆寫供應商,執行期能熱重載停用某一家。離線 demo 把 guardrail 設成 mock、線上除錯臨時用 header 換掉、設定檔被改錯一行,城門當下就是敞開的。第二個弱點 Day 16 也講過:注入偵測涵蓋中英文常見句式,也做了正規化,但仍是列舉式的句式比對,碰到編碼變形或純語義改寫就會漏,遮罩同理。守門不是不會錯,是一定會漏。

把這兩件事放在一起就得到一個讓人不安的結論:如果整套合規(個資不落地、注入不進模型)只靠入境這一道關撐著,那麼任何一次「關掉」或「漏掉」,都會直接變成一個合規事故。原文會傳進模型、會寫進日誌、會留下我們最不想留的那種足跡。對一座企業內部閘道來說,這種「單點失效就出事」的結構,本身就是設計缺陷。

三道關加一道保底:把防線疊起來

縱深防禦的思路很古老,軍事上叫「層層設防」:與其指望第一道戰壕擋住所有人,不如讓突破第一道的敵人,迎面撞上第二道、第三道。落到這套平台上,同一份輸入會在它的旅程裡被反覆檢查,而且每一道檢查都當作「前面那些可能都沒做」來運作。

把整條防線畫出來,長這樣——圖中的 L1、L4 是替請求處理鏈編的層號,L1 是貼著入口與出口的最外層,L4 是緊貼模型的最內層:

 行員輸入(原始,含 PII / 可能含注入)
        │
        ▼
 ┌──────────────────────────────────────────┐
 │ L1 入境 Guardrail   擋注入 → 遮 PII        │  ← Day16 / Day17
 └──────────────────────────────────────────┘
        │  (已遮罩,理論上乾淨)
        ▼
 ┌──────────────────────────────────────────┐
 │ L4 送 LLM 前 Gateway  再擋注入 → 再遮 PII   │  ← 緊貼模型的最後一關
 └──────────────────────────────────────────┘
        │  (送進模型的提問,保證看不到原始個資)
        ▼
   [ LLM 生成回答 ]
        │
        ▼
 ┌──────────────────────────────────────────┐
 │ L1 出境 Guardrail   遮 PII(明天 Day19)   │  ← 模型可能回吐個資
 └──────────────────────────────────────────┘
        │
        ▼
 ┌──────────────────────────────────────────┐
 │ Audit 保底遮罩      寫入前再遮一次           │  ← Day25 單獨講
 └──────────────────────────────────────────┘
        │
        ▼
   append-only 日誌(只該落下遮罩後的版本)

數一下就知道為什麼我把今天的機制歸納成「注入兩攔、PII 兩遮、audit 保底」:

關卡 位置 對注入 對 PII 何時講
L1 入境 Guardrail 入境邊界 攔(第 1 次) 遮(第 1 次) Day 16 / Day 17
L4 送 LLM 前 Gateway 緊貼模型 攔(第 2 次) 遮(第 2 次) 本篇
L1 出境 Guardrail 模型回吐 遮(模型回吐,另一回事) Day 19
Audit 保底 寫入邊界 遮(再遮一次) Day 25

注入被攔兩次(L1 入境、L4 送 LLM 前),PII 被遮兩次(L1 入境、L4 送 LLM 前;出境那次是模型回吐的另一回事),最後 audit 在寫入邊界還會再遮一次。

關鍵:幾道關必須共用同一套偵測規則

這裡藏著今天最關鍵、也最容易被做歪的一點:這幾道關必須共用同一套偵測規則。這套平台把遮罩規則和注入句式集中在一支共用的偵測元件裡——八類 PII 各自的遮罩形狀、那套中英文注入句式清單,都只寫在這一個地方。L1L4 不是各自抄一份規則來用,而是都去呼叫同一支。

為什麼這件事這麼重要?因為如果讓每一層各寫各的偵測邏輯,它們會漂移(drift)

  • 今天有人在 L1 補了一條「身分證」規則,忘了在 L4 也補。
  • 明天 L4 把員編的遮罩符號從 *** 改成 [ID]L1 沒跟上。

久了之後,「兩道關」名義上都在,實際上守的東西卻不一樣,縫隙就從這些不一致裡長出來。共用一份規則,等於保證「每一層攔的、遮的,是同一套標準」,疊出來的防線才真的是同一條線的加厚,而不是兩條歪七扭八、彼此露餡的線。

但共用規則有個盲點:漏報完全相關

這裡得補一句,免得把「共用規則」當成純好事。縱深防禦的經典前提是各層機制不同、漏洞不相關——第一道用的方法漏掉的,第二道因為原理不同而可能補得到。可是 L1L4 共用的是同一份 regex 句式:某個注入變體 L1 沒比中,L4 拿同一份規則去比,保證也比不中。換句話說,在「漏報」這個維度上,這兩層是完全相關的——名義上兩道關,真正的偵測力其實只有一層。第二層擋得住的是「L1 被整個關掉」這種失效,擋不了「L1 規則本身漏掉」這種失效。

所以「共用規則」省掉的是 drift(兩邊實作不一致),不是漏報。要讓縱深防禦在「漏報」這個維度也真的有縱深,得異質疊加:讓不同層用不同原理的偵測——一層 regex 句式、一層語意分類器、一層流量異常偵測——這樣 A 漏的 B 才有機會接住。目前這套是「同一份規則跑兩次」,補的是一致性,不是獨立性;走到真正的縱深,異質偵測是還沒做的下一步。

用設定語言把它說具體

下面這份設定,把入境守門整個關掉了:

portal:
  guardrail:
    provider: mock      # 入境 L1 形同虛設:不擋注入、不遮 PII
  audit:
    provider: postgres  # 但稽核照寫資料庫

在沒有縱深防禦的世界裡,這份設定就是一場災難:行員打進來的「員編 123456」會原封不動傳進模型、寫進 PostgreSQL 那份稽核紀錄(audit log)。但這套平台不會就此失守——L4 送 LLM 前的 Gateway 會再遮一次(模型還是看不到 123456),audit 寫入前還會保底再遮一次(日誌裡落下的仍是 ***)。L1 整個失守,後面兩道照樣把該守的守住。對照日誌看:

2026-06-22 09:15:03.402  INFO [req=a1b2c3d4] Guardrail   : provider=mock, input pass-through (no mask)
2026-06-22 09:15:03.404  INFO [req=a1b2c3d4] LlmGateway  : pre-flight mask applied pii=[EMPLOYEE_ID]
2026-06-22 09:15:03.661  INFO [req=a1b2c3d4] AuditLog    : write-boundary mask applied pii=[EMPLOYEE_ID]

第一行明明白白寫著 L1 放水了(pass-through (no mask)),但第二行、第三行各自獨立地把員編遮了起來。這就是縱深防禦運轉時的樣子:

每一層都對自己即將送出去的東西負責,而不去假設上游已經處理好了。

L4 不相信 L1 一定遮過,所以自己遮;audit 不相信前面任何一道一定遮過,所以寫入前自己再遮。這個「不信任上游、自己保底」的原則,正是 audit 那道保底之所以存在的理由——而那道保底本身有一段真實的合規漏洞修補故事,牽涉到「守門被設成 mock 時原文怎麼漏進日誌」,分量夠單獨講一天,我們留到 Day 25 再完整拆開。

這裡得把一個讀者一定會卡住的點點破:既然剛說 L1L4 共用同一支偵測元件,為什麼一個 portal.guardrail.provider: mock 只關掉了 L1L4audit 卻照遮?關鍵在**「共用的是規則資料,不是開關狀態」——三道關呼叫的是同一份偵測元件(同一套規則與遮罩邏輯),但它們是三個獨立的步驟、各有各的開關**:

  • L1 讀的是 portal.guardrail.providermock 關掉的只有這一步。
  • L4 那道送 LLM 前的遮罩在 LLM gateway 層,由另一個設定(portal.llm-gateway.provider)管,根本不看 guardrail.provider
  • audit 那道更乾脆:程式裡寫死了寫入前一定遮一次,不看任何 provider 設定——個資不落地是合規硬要求,不能交給一個可被關掉的開關決定。

所以 mock 關掉的只是 L1 這一步,後兩道在自己的層、用自己的開關(或根本沒有開關)照跑——這正是「每層自負其責」落到設定上的樣子:同一套規則,三個彼此關不掉的閘。

一個中心、三個各自帶開關的閘,畫出來就不會再搞混:

https://ithelp.ithome.com.tw/upload/images/20260819/20183385xuaSughyca.png

不過縱深防禦補的是「萬一上游漏了、關了」,它不該被讀成「所以上游被關掉也無所謂」。有一個比「層層保底」更釜底抽薪,但這套目前沒回答的問題:正式環境到底該不該允許 guardrail = mock 保底是為了容錯,不是為了容忍。

一個「production profile 啟動時偵測到 guardrail=mock 就直接 fail-fast、拒絕啟動」的設計,比層層下游保底更根本。事實上 Day 25 那個真實 bug 的根因,正是「守門可以被設成 mock」這件事本身;縱深防禦沒有解決它,是在容忍它。比較完整的姿態是兩者並存:production 啟動時就擋下危險組合(guardrail=mock、尤其 guardrail=mock 還配著持久稽核),同時保留下游保底當最後一張網。這道 prod 啟動守門目前還沒做——Day 14 也提過同一件事的另一面:逐請求用 header 把守門換成 mock,同樣沒有任何把關。

重複處理的成本,換來「不把合規寄託在上游」

縱深防禦不是免費的,把代價攤開講才公平。

最直接的代價是重複計算。同一段輸入,PII 偵測跑了不只一次:L1 跑一次、L4 又跑一次、寫稽核前再跑一次。每一次都是真實發生的字串掃描與正規表示式比對。從純效能帳本看,這是浪費——理論上 L1 做完,後面兩道面對的已經是乾淨字串,再掃也掃不出東西。我們明知這件事,還是選擇重複做。

為什麼這筆帳划算?因為兩邊秤的根本不是同一種東西:

省下的一側 賭上的一側
秤的是什麼 幾次正規表示式比對的 CPU 時間 「個資不落地」這個合規承諾
量級 微秒等級、幾乎可忽略;何況 Day 6 立過的鐵律保證純計算不會卡住事件迴圈 整條合規線會不會在某次設定失誤裡崩掉

一邊是省幾微秒,一邊是賭上整條合規線——這個天平怎麼壓,其實沒得選。

更深一層的取捨,是關於「合規責任該放在哪」。一個偷懶但常見的設計是:讓上游負責清乾淨,下游直接信任。這在內部模組之間或許還行,但守門這件事的特性是——它隨時可能被合法地關掉或換掉。把「個資已經遮過」當成下游可以依賴的前提,等於把整套合規架在一個會變動的設定狀態上。縱深防禦的選擇剛好相反:不把任何一層的正確性,寄託在另一層有沒有正確運作。每一層守好自己的出口,這樣就算上游被關掉、被換掉或單純漏了,防線也只是變薄,不會破洞。

當然這也不是說可以無限疊關卡。每多一道,就多一份維護成本、多一個要跟「共用規則」對齊的點、也多一次計算。這套平台的選法是按「資料即將流向哪裡」來設防:

  • 流進模型前設一道L4)——因為一旦送出去就脫離我們掌控。
  • 流進永久日誌前設一道audit 保底)——因為日誌是 append-only、寫錯了改不掉。

設防的位置不是隨便堆的,是盯著那些「資料一旦越過就收不回來」的邊界。把關卡擺在這些不可逆的出口上,每一道的存在就都有它非守不可的理由。

明天 Day 19,我們把目光從入境轉到出境——模型自己吐出來的東西也可能帶風險,但出境這道關有個入境沒有的判斷:有些風險遮一遮就能放行,有些則非整則攔下不可,這就是「可遮蔽 vs 必攔截」。


上一篇
Day 17|入境守門㊁:PII 遮罩
系列文
轉生到全端工程師沒多久就要負責公司的大平台??18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言