Skip to main content

微服務安全:從 API Gateway 到微服務間的授權防禦體系

Updated 9 min readMarkdown
微服務安全:從 API Gateway 到微服務間的授權防禦體系

前言

「API 有 OAuth2、服務都在 Kubernetes 叢集內」常被當成安全的終點,但有兩種攻擊會直接穿過這層防線:被入侵節點的橫向移動,以及 OWASP 列為 API 頭號風險的 BOLA。

本文用一個常見的 BFF(Backend for Frontend)架構,依序回答四個問題:

  1. 一個節點被駭入後,攻擊者能走多遠?
  2. 合法的 OAuth2 access token 能不能擋住 BOLA?
  3. 服務間 mTLS 能防到哪裡?
  4. IETF 正在制定的 Transaction Tokens(Txn-Token)能再補上多少?

一個節點被駭,攻擊者能走多遠

爆炸半徑不是被攻破的那個服務,而是它直接與間接能連到的所有服務。

典型架構是這樣的:BFF 是唯一對外的入口,背後呼叫一群內部服務,內部服務之間也互相呼叫。叢集內部預設互信,只要網路連得到就能呼叫。

BFF 一旦被攻破,攻擊者就繼承了 BFF 在叢集內的身分與網路可達性。接著每個被觸及的服務都能當成下一個跳板,一路從訂單、庫存、支付走到帳務服務與資料庫。下圖中紅色虛線是攻擊者從 BFF 出發的橫向移動路徑:只要服務間預設互信,每個被觸及的服務都會成為下一個跳板。

BOLA:合法 token 擋不住的漏洞

OAuth2 access token 擋不住 BOLA,因為它證明的是「你是誰、能做哪一類操作」,而 BOLA 出問題的地方是「你能存取哪一筆資料」。

BOLA(Broken Object Level Authorization)在 OWASP API Security Top 10 2023 版排名第一。看一個典型情境:

  1. Alice 拿著合法的 token(scope 是 orders:read)呼叫 GET /orders/1001,正常。
  2. 她把 ID 改成 1002,那是 Bob 的訂單。
  3. token 簽章正確、沒過期,scope 也符合,每一關都通過。
  4. API 沒有檢查訂單 1002 的 owner 是不是 token 的 sub,於是回傳了 Bob 的資料。

scope 表達的是「可以讀訂單」,不是「可以讀哪幾張訂單」。把物件清單塞進 token 也不實際:資料會變動,token 會變肥。就算用 Rich Authorization Requests(RFC 9396)描述更細的權限,最後還是要由 resource server 自己執行檢查。

簡單說,OAuth2 是必要條件,不是充分條件。

延伸觀念:BFLA、BOLA 與 BOPLA 的防線分工

把 BOLA 放回 OWASP API Security Top 10 的脈絡來看會更清楚。OWASP 把授權漏洞拆成三個維度,各自對應到不同的防禦邊界:

弱點名稱(OWASP Top 10) 核心維度 檢驗問題 典型情境 本文防線定位
BFLA
(API5:2023 Function Level)
垂直(功能級) 誰能執行這項功能? 一般使用者直接發送 POST /api/admin/users/delete,後端沒擋權限直接刪除帳號。 OAuth2 Scope 可攔截:只要 API Gateway 或服務檢查 Scope / Role 即能阻斷。
BOLA
(API1:2023 Object Level)
水平(物件級) 誰能存取這筆資料? Alice 持合法 Token 將 GET /orders/1001 改為 1002,成功看到 Bob 的訂單。 OAuth2 / mTLS 盲區:需靠微服務查詢時強制比對 owner_id = sub。
BOPLA
(API3:2023 Property Level)
屬性(欄位級) 誰能讀取/修改物件的特定屬性? 1. 過度暴露:API 回傳了不該給的 password_hash。
2. 大量指派/竄改:更新時偷塞 "role": "admin",或中繼節點竄改轉帳帳號。
Txn-Token tctx 可鎖定交易屬性:用簽章防止中途竄改;讀取面則靠 DTO 白名單。

拆開來看就能發現:OAuth2 Scope 本質上是防 BFLA(能不能打這支 API),看不見資料歸屬;Txn-Token 則進一步把 sub 與 tctx 交易參數綁死帶進內部,幫忙擋掉中途的身分冒充與參數竄改(BOPLA)。至於 BOLA,最後一哩路還是得落在微服務查資料庫的那一刻(WHERE owner_id = sub)。

在 BFF 架構中,兩種風險會疊加

最危險的寫法是「BFF 驗完 token,內部服務就完全信任 BFF」。

常見做法是 BFF 用自己的 service account 呼叫內部服務,把 userId、orderId 放在 header 或 query 裡轉發,內部服務不再檢查。這會產生兩個後果:

  • BFF 直接轉發使用者給的 ID,BOLA 就穿透到內部服務。
  • 任一節點被入侵後,攻擊者能帶著任意 X-User-Id 呼叫其他服務,內部服務無從分辨請求是不是真的代表那個使用者。

兩者會互相放大:BOLA 讓合法使用者越權,橫向移動讓入侵者冒充任何使用者。

mTLS:證明「呼叫者是哪個 workload」

mTLS 能把橫向移動的範圍縮小到合法的呼叫路徑,但它只認得 workload、不認得使用者,所以對 BOLA 沒有幫助。

mTLS 讓連線兩端都出示憑證、互相驗證,並加密服務間流量。在 Kubernetes 上,每個 workload 通常會拿到一個與 ServiceAccount 綁定的身分,例如 SPIFFE ID spiffe://prod.internal/ns/shop/sa/bff。

光開 mTLS 還不夠

mTLS 本身只做認證,不做授權。只開 mTLS 的話,任何持有合法憑證的 workload 仍然可以呼叫任何服務,上面那張圖的紅色虛線一條都不會少。

要搭配呼叫政策,明確寫出「誰能呼叫誰」。以 Istio 為例,先用 PeerAuthentication 的 STRICT 模式強制 mTLS,再用 AuthorizationPolicy 依來源身分放行:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: order-service-allow-bff
  namespace: shop
spec:
  selector:
    matchLabels:
      app: order-service
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["prod.internal/ns/shop/sa/bff"]

一個 workload 只要套用了 ALLOW 政策,沒有符合任何規則的請求就會被拒絕。這樣一來,只有實際需要的呼叫邊會留下來。

mTLS 的天花板

被入侵的 BFF 呼叫訂單服務,是一條「本來就被允許」的路徑,身分完全合法,政策攔不下來。mTLS 也不知道這通呼叫代表哪位使用者,攻擊者在合法路徑上帶任意 X-User-Id,對方一樣無從分辨。

換句話說,mTLS 回答「誰在打電話」,下一節的 Txn-Token 回答「這通電話代表哪位使用者的哪筆交易」。兩者互補;Txn-Token 草案本身也要求 workload 與 TTS 之間雙向認證,並建議使用 mTLS 或 SPIFFE 這類機制。

Transaction Tokens 是什麼

Txn-Token 是一張短效、有簽章的 JWT,把「誰、要做什麼、在什麼情境下」不可篡改地帶過整條服務呼叫鏈。

它是 IETF OAuth 工作小組的草案 draft-ietf-oauth-transaction-tokens。撰文時為 draft-11(2026-07-30),工作小組已達成共識、等待 write-up,預計 2026 年 12 月送 IESG。它還不是 RFC,欄位名稱在定稿前仍可能變動。

用看診來理解

把一次 API 請求想成到醫院看診,對應關係是這樣的:

  • 健保卡是 access token。 掛號櫃台(對外入口)驗證後,健保卡就不再往後傳。
  • 醫囑單是 Txn-Token。 醫生(TTS)開出一張蓋了院內防偽章的醫囑單,寫明病人是誰、這次要做哪些檢驗或給哪些藥,只限這次看診有效。
  • 醫囑單在各科室之間流轉。 檢驗科、放射科、藥局就是內部服務,每個科室都先核對印章,再照單執行。病人不需要,也不能修改這張單。
  • 沒有人能中途改單。 藥局不能把病人名字換掉(sub),檢驗科也不能自己多加項目(tctx)。

這個比喻也剛好說明了 Txn-Token 的三個侷限:

  • 醫生開錯藥,醫囑單還是會照樣執行。 印章保證內容沒被改,不保證內容是對的,這就是它擋不住 BOLA 的原因。
  • 防偽章被偷,蓋出來的單看起來都是真的。 TTS 和它的簽章金鑰是整套機制的根本。
  • 醫囑單可以被影印重用,直到過期。 Txn-Token 無法偽造,但拿到的人就能在有效期內重放。

運作流程

  • 外部用戶端帶著 access token 呼叫對外入口(BFF 或 API gateway)。
  • 入口用 OAuth 2.0 Token Exchange(RFC 8693)向信任域內唯一的 Transaction Token Service(TTS)換一張 Txn-Token,雙方必須相互認證。
  • TTS 簽發短效(分鐘級或更短)、typ 為 txntoken+jwt 的 JWT。
  • 之後每一跳都用 Txn-Token HTTP header 原封不動往下傳。規格刻意不用 Authorization header,因為那個位置可能另有服務間授權用途。
  • 每個收到請求的服務自己驗簽章、aud 與 exp,並依 tctx 檢查參數與執行授權,再決定要不要放行。Txn-Token 絕不回傳給外部用戶端。

透過這套換票機制,原本用於南北向的外部憑證,在進入叢集邊界後就轉換為東西向專用的交易憑證:

常見 Token 屬性與 Claims 解析

在探討 Txn-Token 之前,先快速梳理現代 Token(JWT / OAuth2 / OIDC)中最常見的標準屬性(Claims)。如果把 Token 想成一張數位通行證,這些屬性回答了幾個根本問題:

1. RFC 7519 標準 Claims(身分與生命週期)

Claim 全名 白話意義 資安防護重點
iss Issuer(簽發者) 是誰發的? 防止假冒機構:接收端必須比對 iss 是否為信任的 IdP/TTS,並以此拉取對應的 JWKS 公鑰驗簽。
sub Subject(主體) 代表誰? 唯一身分標識:代表使用者或背景服務,應使用不可變的內部唯一 ID(如 UUID),而非可變的帳號或 Email。
aud Audience(受眾) 這張票給誰看的? 防橫向轉發(極為關鍵):限制只有目標信任域或服務能收。若不檢查,被竊取的 Token 容易被轉發攻擊其他無關服務。
exp Expiration Time(過期時間) 何時失效? 限縮外洩窗口:以 Unix Timestamp 標記。Txn-Token 要求短效(分鐘級甚至秒級),降低重放利用空間。
iat Issued At(簽發時間) 何時發的? 判定權限新鮮度:協助驗證 Token 年齡,亦可用於強制註銷(如拒絕密碼變更前簽發的所有 Token)。
nbf Not Before(生效時間) 何時才能用? 提早發放但指定時限後才啟用的預約票券(一般即時請求通常省略)。
jti JWT ID(唯一流水號) 每張票的防偽編號 防止重放攻擊(Replay Attack):接收端可在快取中記錄已出現過的序號,阻斷短時間內的重複請求。

2. OAuth 2.0 / OIDC 常見授權屬性

  • scope(權限範圍):描述「被授權執行哪一類操作」(例如 orders:read)。但它只定義粗粒度的動作類型,無法約束細緻到「能存取哪一筆資料」(這就是為什麼光靠 scope 擋不住 BOLA)。

    實務補充:Scope 的命名與治理 RFC 6749 其實沒有限制 Scope 的字串格式,只規定用空格分隔。但實務上若放任各微服務自己取名,很容易撞名(例如兩個服務都定義 read)。比較穩健的做法是由 API Gateway 或授權中心集中維護 Scope 清單(Scope Registry),並採用 <資源>:<動作>(如 orders:read)的前綴慣例來隔離命名空間;跨服務時也可搭配 RFC 8707 (Resource Indicators) 限制 Token 的適用受眾,避免一張 Token 塞滿所有服務的權限。

  • client_id / azp(Authorized Party):標記當初是「哪一個前端 Client / App」(如行動 App 或 Web SPA)替使用者發起換票。

3. Txn-Token 的專屬 Claims

IETF Transaction Tokens 在標準 JWT 的基礎上,額外擴充了微服務內部最缺乏的交易脈絡:

Claim 全名 用途
txn Transaction ID 整條微服務呼叫鏈的唯一交易識別碼(由 TTS 簽章背書,相當於具密碼學保證的進階 jti)。
tctx Transaction Context 整條鏈路上不可篡改的業務參數(例如 order_id、tenant_id),直接由 TTS 簽章鎖定。
rctx Request Context 請求當下的環境資訊(例如原始客戶端 IP req_ip、認證強度 authn: mfa)。
req_wl Requesting Workload 向 TTS 申請這張 Token 的工作負載身分(例如 bff),記錄最初是由誰帶入叢集。

一張包含標準身分與交易脈絡的 Txn-Token payload 大致如下:

{
  "iss": "https://tts.prod.internal",
  "aud": "prod.internal",
  "sub": "alice",
  "iat": 1793750100,
  "exp": 1793750400,
  "txn": "97053963-...",
  "scope": "orders.read",
  "req_wl": "bff",
  "tctx": { "order_id": "1002" }
}

[!NOTE] 跨信任域考量:Txn-Token 的 aud 限制其只能在當前信任域內流通,不可跨域攜帶。若呼叫鏈涉及跨信任域,草案建議改用正在制定中的 draft-ietf-oauth-identity-chaining 進行身分與授權串聯。

其他場景

tctx 和 scope 裡要放什麼,草案沒有規定,由各自的信任域決定。下面四個例子示範不同場景的寫法,欄位名稱皆為示意。

轉帳:鎖住金額與收款人

{
  "aud": "prod.internal",
  "txn": "5f1c2a90-...",
  "sub": "alice",
  "scope": "transfer.create",
  "req_wl": "bff",
  "rctx": { "req_ip": "203.0.113.10", "authn": "mfa" },
  "tctx": {
    "from_account": "acct-001",
    "to_account": "acct-778",
    "amount": "5000",
    "currency": "TWD"
  }
}

途經的風控、帳務、通知服務看到的是同一組金額與收款帳戶,被入侵的節點無法把 to_account 換成自己的帳戶。風控服務也能直接讀 rctx.authn,確認這筆轉帳有經過多因素驗證。這本質上就是在微服務鏈路中,利用密碼學鎖死交易關鍵欄位,徹底防範中繼節點發起的 BOPLA(屬性竄改 / Mass Assignment)。

多租戶 SaaS:把租戶邊界帶進每一跳

{
  "aud": "prod.internal",
  "txn": "c03e7d11-...",
  "sub": "bob@acme",
  "scope": "files.download",
  "req_wl": "api-gateway",
  "tctx": {
    "tenant_id": "acme",
    "file_id": "f-3391"
  }
}

儲存、掃毒、稽核等下游服務都用 tctx.tenant_id 過濾資料,不必相信請求參數裡的租戶 ID。但這仍然不能取代 BOLA 檢查:檔案服務還是要確認 f-3391 屬於 acme,而且 bob 有權下載。

列表與搜尋情境(List / Batch Read):鎖定查詢邊界

{
  "aud": "prod.internal",
  "txn": "d8e31a22-...",
  "sub": "alice",
  "scope": "orders.list",
  "req_wl": "bff",
  "tctx": {
    "tenant_id": "acme",
    "department_id": "sales",
    "max_limit": 50
  }
}

面對 GET /orders?limit=50 這種沒有單一資源 ID 的列表或批量撈取請求,tctx 鎖定的是查詢邊界與過濾維度。需要特別留意的是:

  • max_limit 必須由 TTS 依政策設定上限(例如強制限制最多 50 筆),不能直接照抄呼叫端的請求參數;否則被攻破的 BFF 只要申請 max_limit: 100000,TTS 就會忠實簽出。
  • department_id 等權限屬性應由 TTS 從身分目錄或授權系統計算出來,而非來自請求參數(草案範例即包含這類由 TTS 自行計算、不在原始請求中的值)。

下游資料服務將 tctx 的過濾條件強制注入 SQL 的 WHERE 與 LIMIT 子句中,即可防止上游節點竄改查詢邊界(例如把條件竄改為 ALL 或惡意拉大分頁範圍)。但請注意:持有資料庫憑證的服務一旦被入侵,Txn-Token 無法保護資料庫本身,仍需靠資料庫帳號權限最小化與存取稽核。

內部排程:沒有使用者請求的交易

{
  "aud": "prod.internal",
  "txn": "8a62b4e5-...",
  "sub": "invoice-job",
  "scope": "invoice.generate",
  "req_wl": "billing-scheduler",
  "tctx": {
    "billing_period": "2026-10",
    "tenant_id": "acme"
  }
}

每月產生帳單的排程沒有外部 access token,可以改用自簽 JWT(self_signed)向 TTS 申請。這種申請方式的 sub 是由申請者自己決定的,所以 TTS 的簽發政策要嚴格限制:只有這個排程 workload 能用,而且只能拿到 invoice.generate。

TTS 怎麼驗證申請者

TTS 會分開檢查兩件事:「誰在申請」與「申請的是誰的交易」。自簽 JWT 只負責後者,它是 workload 用自己的金鑰簽的,單獨拿出來沒有證明力;真正的信任來自申請者的身分認證,再加上 TTS 的簽發政策。

第一步:雙向認證。 草案要求 TTS 與申請的 workload 必須互相認證,並建議用非對稱方式,例如 mTLS client 認證(RFC 8705)或簽章的 client assertion(RFC 7523),憑證可由 SPIFFE 發放。以帳單排程為例,SPIRE 給 invoice-job 發一張 X.509-SVID,排程用它以 mTLS 連到 TTS,TTS 驗證憑證鏈後就知道「打進來的是 invoice-job」。反方向同樣重要:workload 也要驗證 TTS 的伺服器憑證,並只從可信來源取得 TTS 位址,避免把 token 送給冒牌的 TTS。

第二步:送出申請。 排程先做一張自簽 JWT,草案要求它包含 iss(申請的 workload)、sub(交易主體)、aud(TTS 本身)、iat 與 exp:

{
  "iss": "spiffe://prod.internal/ns/billing/sa/invoice-job",
  "sub": "invoice-job",
  "aud": "https://tts.prod.internal",
  "iat": 1793750400,
  "exp": 1793750460
}

用自己的 SVID 私鑰簽好後,透過 mTLS 連線送出 token exchange 請求(實際送出時參數需 URL encoding,這裡為了好讀省略):

POST /token HTTP/1.1
Host: tts.prod.internal
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:txn_token
&audience=prod.internal
&scope=invoice.generate
&subject_token=<自簽 JWT>
&subject_token_type=urn:ietf:params:oauth:token-type:self_signed
&request_details={"billing_period":"2026-10","tenant_id":"acme"}

第三步:TTS 依序檢查。

  1. 申請者是誰:從 mTLS 憑證得知是 invoice-job,無法認證就拒絕。
  2. 自簽 JWT 是否有效:用該 workload 的公鑰驗簽,確認 aud 是自己、iat 與 exp 合理。實務上還應確認 iss 與 mTLS 認證出來的身分一致,避免 A 拿著 B 簽的 JWT 來申請;這一條草案沒有逐字規定,但很建議加上。
  3. 簽發政策:這個 workload 能不能用 self_signed、能用哪些 sub、能拿到哪些 scope。
  4. 全部通過才簽發 Txn-Token,並把 req_wl 設成 invoice-job。

第 3 步是關鍵。草案規定,如果 TTS 無法從 subject token 或其他可信來源判斷原始權限範圍,就必須拒絕。自簽 JWT 本身沒有 scope,所以這個可信來源只能是 TTS 自己的政策。政策格式由各實作決定,以下僅為示意:

- workload: spiffe://prod.internal/ns/billing/sa/invoice-job
  subject_token_types: [self_signed]
  allowed_sub: [invoice-job]
  allowed_scope: [invoice.generate]

如果 invoice-job 被入侵,攻擊者能拿到合法憑證,也能自己簽 JWT,TTS 擋不住它申請,只能靠政策限制它拿得到什麼。sub 鎖死成自己、scope 只有 invoice.generate,它最多就只能產帳單。最危險的設定,是允許某個 workload 用 self_signed 填入任意使用者的 sub,那等於讓它可以冒充任何人。

草案也特別強調:Txn-Token 不是 access token,也不是 workload 的認證憑證;而且禁止把使用者原本的 access token 放進 Txn-Token 裡。

Txn-Token 能擋什麼、擋不住什麼

Txn-Token 主要對準橫向移動,對 BOLA 只補上一半。

對 BOLA:可信的身分,但不判斷意圖合不合法

它解決了前面提到的「轉發 X-User-Id」問題:內部服務拿到的是有簽章、不可篡改的 sub,中途節點也無法偷換 tctx 裡的 order_id。

但如果 Alice 一開始就請求 Bob 的訂單 1002,TTS 只會忠實地簽出「alice 要讀 1002」。Txn-Token 保證的是意圖沒被篡改,而「服務如何根據 token 做授權」被草案明確列為規格範圍外。訂單服務仍然要檢查 owner_id = sub,只是現在能用可信的身分來做。

防禦後的架構對照

當我們把 mTLS + 授權政策、Txn-Token 以及 服務層物件授權 分層疊加後,前面那張橫向移動與越權的攻擊圖就被收斂成如下的零信任防線:

綠色實線代表具備身分與交易脈絡的合法通訊;紅色虛線則是原本在缺乏防禦時會成功的橫向移動與越權路徑。在加入防禦後,這些攻擊大多被 mTLS 呼叫政策、Txn-Token 參數錨定以及物件授權攔截(註:路徑 ② 假設 BFF 業務上不需直接呼叫支付服務;若業務需要則仍為合法連線,政策無法攔阻,需仰賴支付服務本身的驗證;重放攻擊與入口節點淪陷風險依然存在,需搭配進一步配套)。

mTLS 與 Txn-Token 逐項對照

情境 mTLS + AuthorizationPolicy Txn-Token
不在允許清單上的 workload 直接呼叫服務 擋得住 擋得住,沒有合法 Txn-Token 就不被接受
被入侵的服務沿著允許的路徑憑空發起呼叫 擋不住,身分合法 擋得住,沒有進行中的交易就沒有 token
內部節點偽造 X-User-Id 冒充別人 擋不住,不認得使用者 擋得住,sub 有簽章
中途節點篡改請求參數 擋不住 擋得住,前提是下游以 tctx 為準,或比對請求參數與 tctx
竊聽或篡改服務間流量 擋得住,加密且有完整性保護 不處理,依賴傳輸層
從 token 裡挖出使用者的 access token 去打別的 API 不處理 擋得住,規格禁止放入 access token
被入侵節點在有效期內重放手上的 Txn-Token 擋不住,若路徑被允許 只能縮小範圍:短效期、窄 scope,用 txn 偵測或一次性檢查
使用者本人改 ID 存取別人的資料(BOLA) 擋不住 擋不住,仍需物件層級授權
BFF 本身被入侵 部分:只能打 BFF 被允許呼叫的服務 部分:仍需有效 subject token;若允許 self_signed 或 unsigned_json,就能替任意使用者申請

重放那一項要特別注意:一次性檢查需要各副本共享狀態,實務上不一定做得到。

實務建議

沒有單一機制能同時解決兩個問題:BOLA 靠每個服務的物件層級授權,橫向移動靠分層的服務間信任。

物件層級授權

  • 由擁有資料的服務自己檢查,不要只靠 BFF 或 gateway。subject 只從驗證過的 token 取,不從 path、body 或自訂 header 取。
  • 把擁有權條件寫進查詢,不要先撈出資料再比對(見下方範例)。
  • 找不到或無權限都回 404,避免洩漏資源是否存在。
  • 分享、角色、組織階層等複雜關係,可以考慮 OPA、OpenFGA(Zanzibar 式 ReBAC)或 Cedar,但呼叫檢查的責任仍在各服務。
  • UUID 只能增加猜測難度,ID 很容易從其他 API 回應、log 或分享連結外洩。
  • 用兩個測試帳號做交叉存取的自動化測試。
claims := auth.ClaimsFrom(ctx) // 來自已驗證的 Txn-Token

// 單一資源查詢:強制比對擁有者
order, err := repo.GetOrder(ctx, orderID, claims.Subject)
// SQL: WHERE id = $1 AND owner_id = $2(多租戶再加 tenant_id)
if errors.Is(err, repo.ErrNotFound) {
    return http.StatusNotFound
}

// 列表與批次查詢:由 tctx 強制帶入查詢邊界與分頁上限,防止上游節點竄改條件
orders, err := repo.ListOrders(ctx, claims.TxnContext.TenantID, claims.TxnContext.MaxLimit)
// SQL: WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2

接收端驗證清單

呼叫鏈上的內部服務收到請求時,應落實以下防禦檢查清單:

  1. 驗證簽章與演算法:以 TTS 的公開金鑰集(JWKS)驗簽,嚴格限制只接受預期的非對稱演算法(如 ES256、RS256),防止演算法降級攻擊(如 none 或非對稱轉對稱攻擊)。
  2. 檢查 Token 類型(typ):確認 Header 中的 typ 欄位為 txntoken+jwt,避免把外部 OAuth2 Access Token 或內部一般 JWT 誤當作 Txn-Token 放行。
  3. 驗證有效性與信任邊界:確認 aud 為本地信任域(如 prod.internal),且當前時間介於有效期間內(檢查 iat 與 exp,確認未過期)。
  4. 比對操作權限(scope):檢查 token 內的 scope 是否涵蓋當前 API 所欲執行的動作。
  5. 校驗交易脈絡(tctx):業務邏輯應直接取用 tctx 內的不可變參數(如 tenant_id、order_id),或比對 HTTP 請求參數與 tctx 是否完全一致;若不一致必須立即拒絕。
  6. 安全轉發與去識別化記錄:往下一跳轉發時以原樣傳遞 Txn-Token Header。禁止在日誌中輸出 Token 原文,僅記錄其 Hash 或關鍵 Claims。

服務間信任的三層

層次 回答的問題 常見做法
網路與呼叫政策 誰能呼叫誰 NetworkPolicy 預設拒絕、Istio AuthorizationPolicy
Workload 身分 呼叫者是哪個 workload mTLS、SPIFFE
交易脈絡 這次呼叫屬於哪位使用者的哪筆合法交易 Txn-Token

把這三層信任加上服務自己的物件授權,單一微服務收到請求時就會依序通過四道關卡:

TTS 簽發政策

  • 限制哪些 workload 能申請 Txn-Token,以及能用哪種 subject token。self_signed 與 unsigned_json 只開給真正需要的排程類 workload。
  • scope 不得超過原始 token;無法判定原始 scope 時,草案要求直接拒絕。
  • 替換 Txn-Token(Token Replacement,草案 Section 13.15): 將 Txn-Token 當作 subject_token 再次向 TTS 換發時,草案語氣偏向保守,強調 TTS 必須特別謹慎,因為頻繁替換可能抵銷 Txn-Token 原有的防禦意圖。
    • 延伸應用情境(Fan-out 縮權):當呼叫鏈分岔時,上游服務可作為一種可能的延伸應用方式(常被非正式稱為巢狀或衍生 Token),拿現有 Txn-Token 向 TTS 申請一張 scope 更窄(例如給支付服務只保留 payments.charge)或在 tctx 補充不可變計算結果的子代 Token 給特定下游。
    • 草案的硬性約束:核心交易脈絡(txn、sub、aud)必須保持一致且不得改動、權限只能縮小不能擴大;已過期的 Txn-Token 絕對禁止換發(MUST NOT);TTS 應嚴格限制替換鏈的深度(Depth limit);並且 TTS 必須保留原呼叫鏈的申請者歷史紀錄(Audit Trail)。

觀測與偵測

  • 以已驗簽的 txn 作為關聯鍵,比 trace_id 更可信: 雖然 W3C Trace Context 的 trace_id 能串起分散式呼叫鏈,但 trace_id 是由呼叫端任意產生、且未經數位簽章保護(任何中繼節點都能輕易偽造或重設,即便在 mTLS 加密傳輸下亦然)。相比之下,Txn-Token 的 txn 是由 TTS 簽章背書的唯一交易識別碼。
    • 建議由「接收並驗簽成功」的那一方記錄 txn,避免未經驗證的上游隨意宣稱。
    • 資安鑑識時,可將服務端記錄的 txn 與 TTS 的簽發紀錄(或 Token Hash)進行交叉比對,建立高可信度的交易因果軌跡。
  • 個資與日誌隱私考量: 不要記錄 Txn-Token 原文,只記 hash 或去掉簽章的 payload。若要將 sub 或 rctx.req_ip 寫入 OpenTelemetry Span Attributes,考量其可能在 Tempo 或 Jaeger 等追蹤後端長期留存且缺乏行級遮蔽,屬於個資(PII),建議呼應草案隱私考量章節,改記 Hash 或假名化 ID。
  • 告警檢查:同一 subject 短時間大量 404 或 403、連號 ID 掃描、同一 txn 出現在不合理的呼叫路徑或時間點。

導入成本與架構權衡

多加一層 Token 交換,工程上勢必得付出代價。實際落地時通常會遇到三個問題:

  • 熱路徑(Hot Path)的延遲與單點故障:TTS 卡在所有外部請求的第一線,如果每次呼叫都要跨網路去向 TTS 換票,TTS 很容易變成效能瓶頸與 SPOF。草案本身允許透過本機介面取票,實務上也建議把 TTS 做成 Sidecar 或 Node Agent(例如跟 Envoy 或 SPIRE Agent 綁在一起),改走本機 IPC / UDS 來省掉網路 Round-trip 開銷。
  • 多語言驗簽 SDK 與金鑰輪替:內部每個服務都要驗簽,代表大家都要處理 JWKS 快取與 Key Rotation。如果沒有提供統一的內部 SDK 或透過 Sidecar 統一處理,各團隊自己手刻驗證邏輯反而容易踩坑。
  • 草案還在演進:目前 draft-ietf-oauth-transaction-tokens 還沒正式成為 RFC,欄位細節仍可能微調,實作時建議把解析邏輯封裝起來,保留一點抽換彈性。

結語

OAuth2 access token 解決的是入口的認證,不是「這筆資料是不是你的」,也不是「這通內部呼叫是不是真的代表使用者」。mTLS 搭配呼叫政策,把爆炸半徑縮到合法的呼叫圖;Transaction Tokens 再把它縮到正在進行的那筆交易。但物件層級授權仍然是每個服務自己的責任,這兩者只是讓這個檢查終於有可靠的依據。

參考資料

No comments yet. Be the first to comment.

More from this blog