
前言
「API 有 OAuth2、服務都在 Kubernetes 叢集內」常被當成安全的終點,但有兩種攻擊會直接穿過這層防線:被入侵節點的橫向移動,以及 OWASP 列為 API 頭號風險的 BOLA。
本文用一個常見的 BFF(Backend for Frontend)架構,依序回答四個問題:
- 一個節點被駭入後,攻擊者能走多遠?
- 合法的 OAuth2 access token 能不能擋住 BOLA?
- 服務間 mTLS 能防到哪裡?
- 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 版排名第一。看一個典型情境:
- Alice 拿著合法的 token(scope 是
orders:read)呼叫GET /orders/1001,正常。 - 她把 ID 改成
1002,那是 Bob 的訂單。 - token 簽章正確、沒過期,scope 也符合,每一關都通過。
- 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-TokenHTTP header 原封不動往下傳。規格刻意不用Authorizationheader,因為那個位置可能另有服務間授權用途。 - 每個收到請求的服務自己驗簽章、
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 依序檢查。
- 申請者是誰:從 mTLS 憑證得知是
invoice-job,無法認證就拒絕。 - 自簽 JWT 是否有效:用該 workload 的公鑰驗簽,確認
aud是自己、iat與exp合理。實務上還應確認iss與 mTLS 認證出來的身分一致,避免 A 拿著 B 簽的 JWT 來申請;這一條草案沒有逐字規定,但很建議加上。 - 簽發政策:這個 workload 能不能用
self_signed、能用哪些sub、能拿到哪些scope。 - 全部通過才簽發 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
接收端驗證清單
呼叫鏈上的內部服務收到請求時,應落實以下防禦檢查清單:
- 驗證簽章與演算法:以 TTS 的公開金鑰集(JWKS)驗簽,嚴格限制只接受預期的非對稱演算法(如
ES256、RS256),防止演算法降級攻擊(如none或非對稱轉對稱攻擊)。 - 檢查 Token 類型(
typ):確認 Header 中的typ欄位為txntoken+jwt,避免把外部 OAuth2 Access Token 或內部一般 JWT 誤當作 Txn-Token 放行。 - 驗證有效性與信任邊界:確認
aud為本地信任域(如prod.internal),且當前時間介於有效期間內(檢查iat與exp,確認未過期)。 - 比對操作權限(
scope):檢查 token 內的scope是否涵蓋當前 API 所欲執行的動作。 - 校驗交易脈絡(
tctx):業務邏輯應直接取用tctx內的不可變參數(如tenant_id、order_id),或比對 HTTP 請求參數與tctx是否完全一致;若不一致必須立即拒絕。 - 安全轉發與去識別化記錄:往下一跳轉發時以原樣傳遞
Txn-TokenHeader。禁止在日誌中輸出 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)。
- 延伸應用情境(Fan-out 縮權):當呼叫鏈分岔時,上游服務可作為一種可能的延伸應用方式(常被非正式稱為巢狀或衍生 Token),拿現有 Txn-Token 向 TTS 申請一張
觀測與偵測
- 以已驗簽的
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 再把它縮到正在進行的那筆交易。但物件層級授權仍然是每個服務自己的責任,這兩者只是讓這個檢查終於有可靠的依據。
參考資料
- draft-ietf-oauth-transaction-tokens-11: Transaction Tokens
- draft-ietf-oauth-identity-chaining: OAuth Identity and Authorization Chaining
- RFC 8693: OAuth 2.0 Token Exchange
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9396: OAuth 2.0 Rich Authorization Requests
- OWASP API Security Top 10 2023: API1:2023 BOLA
Comments
Join the discussionNo comments yet. Be the first to comment.





