4、多租户签名链路需求:签名请求生命周期、租户上下文传递、签名密钥管理
在多租户签名链路中,每个租户的签名请求必须经过完整的生命周期管理,同时确保租户上下文在 Istio 服务网格中透明传递,签名密钥的存储与访问必须严格隔离。本章从三个核心维度展开:签名请求的生命周期阶段、租户上下文的传递机制、签名密钥的分级管理策略。
4.1 签名请求生命周期
一个典型的签名请求从客户端发起至签名结果返回,经历以下五个阶段:
| 阶段 | 触发条件 | 关键动作 | 超时控制 |
|---|---|---|---|
| 1. 请求组装 | 客户端 SDK 调用 | 构造签名原文、附加租户 ID、请求 ID | 100ms |
| 2. 路由与认证 | Istio Ingress Gateway 接收 | JWT 校验、租户路由、速率限制 | 200ms |
| 3. 密钥获取 | 签名服务接收请求 | 从 KMS/HSM 获取租户密钥 | 500ms |
| 4. 签名计算 | 密钥就绪 | 执行 RSA/ECDSA 签名运算 | 300ms |
| 5. 响应回传 | 签名完成 | 封装签名值、更新审计日志 | 100ms |
性能基线: 单次签名请求端到端延迟应控制在 1.2s 以内,其中密钥获取阶段最容易成为瓶颈,建议使用本地缓存 + 异步刷新策略。
下图展示了签名请求在 Istio 网格内的完整生命周期流转:
4.2 租户上下文传递
租户上下文是签名链路中区分不同租户的核心标识。在 Istio 网格中,上下文通过 Envoy 请求头传递,并经过以下三层传递路径:
- 入口层: Ingress Gateway 从 JWT 中提取
x-tenant-id,注入到请求头。 - 网格层: Sidecar Envoy 通过
EnvoyFilter确保请求头不被篡改,并附加租户路由标签。 - 服务层: 签名服务从请求头读取租户 ID,用于密钥查找和审计日志记录。
上下文传递契约:
# 请求头规范
x-tenant-id: tenant-12345
x-request-id: req-abc-20250301
x-signature-algorithm: RS256
# Istio EnvoyFilter 示例(防止租户头被篡改)
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: tenant-header-protection
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.header_mutation
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation
mutations:
- remove: "x-tenant-id" # 禁止下游覆盖
- append: "x-tenant-id" # 仅允许上游注入
下表对比了三种常见的租户上下文传递方式:
| 传递方式 | 安全性 | 性能开销 | 适用场景 |
|---|---|---|---|
| HTTP Header | 中等(需 EnvoyFilter 加固) | 低(无额外编解码) | 标准签名链路 |
| gRPC Metadata | 高(内置 TLS 加密) | 低 | 高性能签名服务 |
| JWT Claim | 高(数字签名验证) | 中(需 JWT 解析) | 跨信任域签名 |
4.3 签名密钥管理
签名密钥是多租户签名链路中最敏感的资源。密钥管理采用三级隔离策略:
- L1 - 密钥存储隔离: 每个租户拥有独立的密钥存储空间(KMS Key Ring 或 HSM Slot),密钥 ID 格式为
projects/{project}/locations/{region}/keyRings/{tenant_id}/cryptoKeys/sign-key。 - L2 - 密钥访问控制: 签名服务使用租户专属的 Service Account 访问 KMS,通过 IAM 条件绑定限制仅能操作本租户密钥。
- L3 - 密钥缓存隔离: 本地缓存使用租户 ID 作为前缀,缓存过期时间按租户独立配置,避免缓存污染。
密钥管理避坑: 禁止在签名服务中缓存明文密钥。应使用 KMS 的
sign 接口直接进行签名运算,密钥材料始终不离开硬件安全边界。缓存仅存储密钥引用(Key URI)和访问令牌,过期时间不超过 5 分钟。
下图展示了密钥获取与缓存隔离的架构:
4.4 密钥轮换与版本管理
签名密钥需要定期轮换以降低泄露风险。密钥版本管理遵循以下原则:
- 主版本(Primary): 当前用于签名计算的密钥版本,所有新签名请求使用主版本。
- 旧版本(Previous): 保留用于验证历史签名,不再用于新签名。
- 轮换策略: 每 90 天自动轮换,轮换期间新旧版本并行存在 7 天,确保签名验证不中断。
密钥轮换最佳实践: 在 Istio 网格中,密钥轮换不应导致签名请求失败。建议在签名服务中实现 双读双写 策略:轮换期间同时持有新旧两个密钥引用,新签名使用新密钥,旧签名验证使用旧密钥。轮换完成后,旧密钥标记为过期并删除。
密钥版本切换的代码示例:
// 密钥版本管理器伪代码
type KeyVersionManager struct {
primary string // 主版本 Key URI
previous string // 旧版本 Key URI(仅用于验证)
mu sync.RWMutex
}
func (m *KeyVersionManager) GetSigningKey(ctx context.Context, tenantID string) (string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// 始终返回主版本用于签名
return m.primary, nil
}
func (m *KeyVersionManager) RotateKey(ctx context.Context, tenantID string) error {
m.mu.Lock()
defer m.mu.Unlock()
// 1. 将当前主版本降级为 previous
m.previous = m.primary
// 2. 创建新密钥版本并设为主版本
newKeyURI, err := kmsClient.CreateCryptoKeyVersion(ctx, m.primary)
if err != nil {
return err
}
m.primary = newKeyURI
// 3. 7 天后清理 previous
go func() {
time.Sleep(7 * 24 * time.Hour)
m.mu.Lock()
defer m.mu.Unlock()
m.previous = ""
}()
return nil
}
4.5 审计与合规
多租户签名链路必须满足审计合规要求。每个签名请求的完整生命周期需要记录以下信息:
| 审计字段 | 来源 | 示例值 |
|---|---|---|
| 租户 ID | 请求头 x-tenant-id |
tenant-12345 |
| 请求 ID | 请求头 x-request-id |
req-abc-20250301 |
| 签名算法 | 请求头 x-signature-algorithm |
RS256 |
| 密钥版本 | KMS 返回 | projects/p/keyRings/k/cryptoKeys/sign-key/cryptoKeyVersions/3 |
| 签名时间 | 签名服务本地时间 | 2025-03-01T12:00:00Z |
| 签名结果 | 签名运算输出 | success / failure |
审计日志安全要求: 审计日志中不得包含签名原文或签名值明文。签名值应存储为哈希摘要(SHA-256),便于后续验证但不泄露原始签名。审计日志存储建议使用不可变存储(如对象存储 + WORM 策略),保留周期不少于 3 年。