你的微服務共享同一個 VPC,因此彼此信任。這種信任就是環境權威:呼叫服務的權限不是由密碼學證明授予的,而是由網路拓撲決定的。一旦攻擊者突破邊界,就會繼承所有這些權限。
服務絕對可以在沒有環境權威的情況下完成認證。更難回答的問題是:為什麼你的平台讓你覺得這是在背叛既有架構。
生產環境中的環境權威長什麼樣
大多數所謂的服務間「認證」,本質上只是網路分段。服務部署在私有子網路、API gateway之後,或者帶有服務網格的 Kubernetes 叢集內部。假設很簡單:如果請求來自內網 IP,它就是合法的。
這個模型在邊界崩塌時隨之崩塌。一個被攻破的 pod、憑證洩露後的橫向移動、或供應鏈注入,都會把攻擊者放進信任區內。此時每個服務都可到達,而且大多數服務不會要求出示身份證明。攻擊者無需竊取更多憑證,只需繼承網路的環境權威即可。
真正的服務間認證意味著:每個呼叫者都必須證明「我是誰」,每個被呼叫者都必須驗證這份證明。網路只是傳輸層,它不應該成為憑證本身。
基於能力的服務認證如何運作
有兩種實用方法:密碼學身份(每個服務都有一個可驗證的名字)和能力權杖(每個請求都攜帶一份不可偽造、作用域受限的授權)。
使用 mTLS 實現密碼學身份
在雙向 TLS 中,用戶端和伺服器都會出示 X.509 憑證。伺服器在處理請求前驗證用戶端憑證;用戶端在傳送資料前驗證伺服器憑證。雙方都不會因為 IP 位址而信任對方。
基礎設施層面的挑戰是憑證分發。SPIFFE 和 SPIRE 透過為每個工作負載賦予一個由其屬性(而非位置)推導出的密碼學身份來解決這個問題。執行 payments-api 的 pod 會獲得類似 spiffe://prod.example.com/payments-api 的 SPIFFE ID。憑證生命週期短,並自動輪換。
以下是 Go 中的實作範例:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
"os"
)
// loadClientTLS returns a config that verifies the server AND presents a client cert.
func loadClientTLS(caCert, clientCert, clientKey []byte) *tls.Config {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caCert)
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
log.Fatalf("failed to load client cert: %v", err)
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: pool,
// Do not skip verification based on IP or DNS name alone.
InsecureSkipVerify: false,
}
}
func main() {
caCert, _ := os.ReadFile("ca.crt")
clientCert, _ := os.ReadFile("service.crt")
clientKey, _ := os.ReadFile("service.key")
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: loadClientTLS(caCert, clientCert, clientKey),
},
}
resp, err := client.Get("https://orders-api.internal:8443/orders")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Status)
}
關鍵行是 InsecureSkipVerify: false。沒有它,你就回到了環境信任。有了它,用戶端只有在伺服器出示由 CA 簽名的有效憑證時才會建立連線;伺服器對用戶端也做同樣的驗證。
作用域受限的能力權杖
憑證證明身份。能力證明授權。在很多系統中,你需要兩者兼具:先證明你是誰,再證明你被允許執行這項具體操作。
能力權杖把身份、動作和資源綁定成一個不可偽造的套件。由服務簽發,由服務驗證。不需要查詢中央會話儲存。
import hmac
import hashlib
import secrets
import time
SECRET = secrets.token_bytes(32)
def mint_service_capability(
caller_id: str,
service: str,
action: str,
resource: str,
ttl_seconds: int = 300
) -> str:
"""Mint a time-bound capability for a specific service action."""
expires = int(time.time()) + ttl_seconds
payload = f"{caller_id}:{service}:{action}:{resource}:{expires}"
sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
return f"{payload}:{sig}"
def verify_service_capability(
token: str,
expected_service: str,
expected_action: str,
expected_resource: str
) -> bool:
"""Verify a capability token is unexpired and correctly scoped."""
try:
caller_id, svc, action, resource, expires, sig = token.rsplit(":", 5)
except ValueError:
return False
if int(expires) < time.time():
return False
if svc != expected_service or action != expected_action or resource != expected_resource:
return False
payload = f"{caller_id}:{svc}:{action}:{resource}:{expires}"
expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:24]
return hmac.compare_digest(sig, expected_sig)
# Service A requests a capability to call Service B.
token = mint_service_capability("payments-api", "orders-api", "read", "order-123")
# Service B verifies the capability before handling the request.
if not verify_service_capability(token, "orders-api", "read", "order-123"):
raise PermissionError("Invalid or expired capability")
權杖是自包含的,且生命週期短。服務 B 無需呼叫授權伺服器,只需檢查 HMAC 和過期時間即可。作用域是明確的:此權杖僅對從 orders-api 讀取 order-123 有效。
為什麼這做起來比想像中困難
技術已經成熟。困難在維運層面。
首先是信任啟動問題。如果每個服務都需要憑證,就需要某個實體來簽發這些憑證。這個實體——憑證授權中心或 SPIRE 伺服器——會成為新的攻擊目標。你並沒有消除信任,只是把信任集中到了一處。
還有撤銷問題。如果某個服務被攻破,如何廢止它的身份?mTLS 憑證可以做得短時效,這有幫助,但撤銷特定憑證仍需要分發憑證撤銷列表或使用 OCSP。能力權杖很快過期,能限制損失,但一旦簽名金鑰被盜,攻擊者就能簽發無限數量的權杖。
效能也是一個顧慮。TLS 交握會增加延遲。在服務網格帶邊車的情境下,額外開銷通常可接受(幾毫秒),但在高吞吐量路徑上,這些毫秒會累積。能力權杖避免了往返授權伺服器,但大規模下的 HMAC 驗證並非零成本。
環境權威在哪些情境下仍然合理
並非所有內部呼叫都需要密碼學證明。與資料庫用戶端跑在同一個 container裡的定時任務,不需要用 mTLS 去連線 localhost。負載平衡器與節點之間的health check,也不需要能力權杖。
目標不是零環境權威。目標是清楚你的環境權威分布在哪裡,以及爆炸半徑是否可接受。一個共享 VPC 裡跑著 200 個服務且沒有內部認證,這是一個巨大且隱形的爆炸半徑。一個本地 Unix 通訊端,檔案權限設為 0600,這是一個小而明確的爆炸半徑。
如何開始移除服務呼叫中的環境權威
你不需要第一天就上線服務網格。先從接觸敏感資料或位於信任邊界的服務開始。
第一,識別你的特權代理。哪些服務擁有其他服務沒有的權限?哪些內部 API 如果被錯誤的服務呼叫會很危險?這些就是你的候選對象。
第二,在 API 層增加 mTLS。如果你使用 Envoy 或 NGINX 這類反向代理,可以在代理層終結 mTLS,而無需改動應用程式碼。代理把一個帶有已驗證用戶端身份的請求頭傳給上游服務,上游服務檢查這個請求頭。
第三,縮小權杖的作用域。如果你已經在用 JWT 做服務認證,停止在載荷裡放 role: service。改成 allowed_services: ["orders-api"] 和 allowed_resources: ["order:*"]。讓授權粒度與操作本身一樣具體。
關於無環境權威服務認證的常見問題
mTLS 夠用嗎,還是我也需要能力權杖?
mTLS 證明身份。能力權杖證明授權。它們解決不同問題。一個擁有有效憑證的服務,仍然不應該被允許隨意刪除資料。用 mTLS 做傳輸層安全和身份認證,用能力權杖做存取控制。
那 Istio 或 Linkerd 這類服務網格呢?
服務網格自動為 Pod 之間的通訊提供 mTLS 和身份管理。這是一種無需重寫每個服務就能移除環境權威的實用方式。網格負責憑證輪換、身份證明和流量加密。代價是複雜性,以及需要保護一個新的控制平面。
這對無伺服器函式也適用嗎?
適用,但更難。無伺服器函式是臨時的,因此憑證輪換和身份證明需要在呼叫時完成。雲端廠商為此提供了工作負載身份(如 AWS IAM Roles for Service Accounts、GCP Workload Identity)。這些機制類似能力權杖:函式收到的是作用域受限的權杖,而不是通用憑證。
服務帳號和能力權杖有什麼區別?
服務帳號是一種預設攜帶環境權威的身份。如果服務 A 以 payments-sa 執行,它通常可以做 payments-sa 被允許做的任何事。能力權杖則被限定到特定資源上的特定動作。即使能力權杖被盜,也無法重放到其他目標上。
從你已有的邊界開始
你的 VPC 從來不是安全邊界。它只是網路邊界,而我們假裝它是安全邊界。移除環境權威意味著把每一次服務呼叫都當作跨越邊界來處理。密碼學身份和作用域受限的能力是工具。最難的部分是承認舊模型只是披著安全外衣的便利。
選一個內部 API。加上用戶端憑證驗證。觀察什麼東西會崩潰。大多數時候,崩潰的是你之前根本沒意識到程式碼在做的一個假設。