你的微服务共享同一个 VPC,因此彼此信任。这种信任就是环境权威:调用服务的权限不是由密码学证明授予的,而是由网络拓扑决定的。一旦攻击者突破边界,就会继承所有这些权限。
服务绝对可以在没有环境权威的情况下完成认证。更难回答的问题是:为什么你的平台让你觉得这是在背叛既有架构。
生产环境中的环境权威长什么样
大多数所谓的服务间“认证”,本质上只是网络分段。服务部署在私有子网、API 网关之后,或者带有服务网格的 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 验证并非零成本。
环境权威在哪些场景下仍然合理
并非所有内部调用都需要密码学证明。与数据库客户端跑在同一个容器里的定时任务,不需要用 mTLS 去连接 localhost。负载均衡器与节点之间的健康检查,也不需要能力令牌。
目标不是零环境权威。目标是清楚你的环境权威分布在哪里,以及爆炸半径是否可接受。一个共享 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。加上客户端证书验证。观察什么东西会崩溃。大多数时候,崩溃的是你之前根本没意识到代码在做的一个假设。