你的微服務共享同一個 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。加上用戶端憑證驗證。觀察什麼東西會崩潰。大多數時候,崩潰的是你之前根本沒意識到程式碼在做的一個假設。