土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】服务身份:mTLS、SPIFFE/SPIRE 与 Workload Identity

文章导航

分类入口
architecturesecurity

目录

在一个典型的微服务系统里,每天发生的调用里只有极少数是”用户发起的”,大多数是服务与服务之间的调用:订单服务调用库存服务、网关调用鉴权服务、Job 读写数据库、Sidecar 上报指标。这些流量过去普遍依赖一类做法——在配置里写一串 SERVICE_TOKEN=xxxxxxxx 的长生命周期 shared secret,或者在 VPC 内部”反正是内网”就直接明文调用。这类做法在规模变大、审计变严、攻击面变复杂之后全部都会破产:密钥轮换困难、一次泄露影响全网、无法精细授权、出了问题也查不出到底是哪个进程发起的请求。

本篇把”服务身份(workload identity)“这件事讲透:为什么要有独立于人的机器身份体系;mTLS 到底要在工程上解决哪些问题;SPIFFE 规范和 SPIRE 实现如何在 Kubernetes 与混合云里给每个工作负载发一个可验证的身份;以及 AWS IRSA、GCP Workload Identity、Azure Workload Identity 这类云原生方案是如何通过 OIDC 把 K8s ServiceAccount 映射到云厂商的 IAM 角色。最后会把 Istio/Linkerd 的 mTLS 串起来——它们本质上就是把 SPIFFE ID 塞进证书 SAN,再用策略控制谁可以调用谁。

SPIRE 架构:从节点认证到工作负载身份

一、服务身份为什么需要独立体系

1.1 人身份与服务身份的根本差别

人身份围绕”会话”展开:一次登录,换一个可撤销的 token,持有几分钟到几小时,结束后销毁。人身份的主要对抗目标是钓鱼、凭据窃取、会话劫持,所以引入了密码、MFA、设备信任、风险引擎等机制。

服务身份的诉求完全不同:

1.2 API Key 与 shared secret 不够用的几个原因

很多团队一开始会用”发一串长 token 放环境变量”的做法。这条路走不远,核心问题有:

  1. 轮换代价巨大:一旦泄露,需要同时更新签发方和所有持有方的配置,往往伴随重启和发布窗口。实际操作中大部分团队根本就不轮换,“反正没事”。
  2. 权限粒度粗:一把 key 往往对应一个完整的账号权限,很难做到”只能读库存,不能写”。
  3. 审计困难:同一个 key 可能被多个服务共用,出了问题查不出是谁在调。
  4. 无法在信任域之间建立关系:跨集群、跨云的调用需要一种可以相互验证的公共标准,token 只是一串字符串,没有签名、没有颁发方信息。
  5. secret 落盘是常态:环境变量、ConfigMap、.env 文件、CI 变量里都会留下痕迹,清理难度大。

服务身份要解决的是:让每一个工作负载在启动那一刻自动获得一个短期、可验证、绑定到其运行环境的密码学凭据,不依赖任何人工配置,也不需要持久保存在磁盘上

1.3 一张表看清差别

维度 人身份(user identity) 服务身份(workload identity)
载体 session cookie、OAuth token X.509 证书、JWT、云端临时凭据
生命周期 分钟到小时 分钟到小时(短期),自动轮换
分发方式 登录交换 平台自动下发
绑定对象 某个自然人 某个工作负载(进程、容器、Pod)
撤销手段 主动退出、黑名单 TTL 到期自然失效
验证方式 口令 + MFA 签名验证 + 运行环境验证
常见攻击 钓鱼、凭据泄露 密钥泄露、权限横向扩散、伪造身份

把服务身份方案选型时,最常见的混淆是把不同层的问题混在一起。可以先用这张边界表做切分:

你要解决的问题 优先方案 说明
服务与服务双向认证 mTLS 先解决「你是谁」和通道加密
给每个工作负载统一身份命名 SPIFFE / SPIRE 解决身份格式、签发链和信任域
让 K8s Pod 获取云厂商临时权限 云厂商 Workload Identity 解决对 AWS / GCP / Azure API 的授权
在 service mesh 中自动下发与轮换证书 Istio / Linkerd + SPIFFE 解决平台化接入与零侵入
细粒度判断谁能调谁 授权策略层(OPA / mesh policy) 身份不等于授权,别混到一层

二、mTLS 工程深度

mTLS(mutual TLS)是服务身份最成熟的载体。客户端验证服务端的同时,服务端也验证客户端,双方都通过证书链证明自己属于某个”信任域”。但把 mTLS 真正跑起来远不只是”开个 flag”那么简单,下面把工程上最常遇到的问题拆开讲。

2.1 证书链、PEM 与 DER

一条完整的证书链通常是三层:

Root CA  (offline,私钥离线保存,20 年有效)
  └── Intermediate CA  (在线,5 年有效,用来签 leaf)
        └── Leaf Cert  (服务或客户端,小时到天级别)

格式上,PEM 是 Base64 编码 + -----BEGIN CERTIFICATE----- 头尾的文本格式,便于复制粘贴和配置文件使用;DER 是二进制的 ASN.1 编码,体积更小、更适合嵌入协议字段。大部分工具支持互转:

openssl x509 -in leaf.pem -outform der -out leaf.der
openssl x509 -in leaf.der -inform der -out leaf.pem
openssl x509 -in leaf.pem -text -noout    # 打印证书内容

一个实际的 leaf 证书片段,注意 SAN 里的 URI 条目:

Subject: CN=orders.prod.example.com
X509v3 Subject Alternative Name:
    DNS:orders.prod.example.com
    URI:spiffe://prod.example.com/ns/orders/sa/orders
X509v3 Key Usage: Digital Signature, Key Encipherment
X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication
Validity: Not Before: 2026-04-20 10:00 UTC
          Not After : 2026-04-21 10:00 UTC   # 24 小时

URI:spiffe://... 这一行是 SPIFFE X.509 SVID 的核心:身份信息通过 SAN URI 表达,不放在 CN 里(CN 已经被广泛认为是过时的身份字段,浏览器和 Go 标准库都只看 SAN)。

2.2 证书签发:CFSSL、Vault PKI、cert-manager

不同规模和场景下主流的签发方式有三类:

CFSSL:Cloudflare 开源的 CA 工具集,轻量、命令行驱动,适合小团队自建 CA。典型用法:

cfssl gencert -initca ca-csr.json | cfssljson -bare ca
cfssl gencert -ca ca.pem -ca-key ca-key.pem \
  -config ca-config.json -profile server \
  server-csr.json | cfssljson -bare server

Vault PKI:HashiCorp Vault 的 PKI secrets engine,支持多租户、可编程的签发策略、审计日志、HSM 集成。生产环境里最常见的”企业级”选择:

vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
vault write pki/root/generate/internal common_name="example.com Root" ttl=87600h
vault write pki/roles/orders \
  allowed_domains="orders.prod.example.com" \
  allow_subdomains=false \
  max_ttl="24h"
# 服务运行时签发
vault write pki/issue/orders common_name="orders.prod.example.com" ttl="24h"

cert-manager:Kubernetes 原生方案,把”Certificate”变成一种 CRD,背后可以对接 Let’s Encrypt(ACME)、Vault、自签 CA、云厂商 CA。典型 Certificate 资源:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: orders-tls
  namespace: prod
spec:
  secretName: orders-tls-secret
  duration: 24h
  renewBefore: 8h
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always
  dnsNames:
    - orders.prod.svc.cluster.local
  uris:
    - spiffe://prod.example.com/ns/prod/sa/orders
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer

rotationPolicy: Always 表示每次续期时连私钥都重新生成,避免长期使用同一把私钥。

2.3 轮换:short-lived 与 long-lived 的抉择

证书轮换有两条路线:

Long-lived + 撤销机制:传统做法,证书有效期几个月到一年,通过 CRL(Certificate Revocation List)或 OCSP(Online Certificate Status Protocol)来处理提前作废。

Short-lived certs:干脆把有效期缩到 1h~24h,轮换成为常态,吊销这件事就不再重要——反正几小时后自动失效。代价是需要一个高可用的签发链路,每天要处理千万级签发请求。SPIFFE/SPIRE、Istio Citadel、HashiCorp Consul Connect 都走这条路线,默认 TTL 1 小时。

生产经验上,只要平台层能把签发做得稳定,short-lived 永远比 long-lived + OCSP/CRL 更省心。CRL 和 OCSP 的代码路径一旦出现 bug,会直接让整套 mTLS 通信瘫痪,排查起来非常痛苦。

2.4 吊销的现实选择

简单总结三种实践:

  1. 短证书 + 快速轮换:最推荐。有效期设到 1~24h,撤销等同于”停止续签”。
  2. CRL distribution point + 定时刷新:适合证书数量不多、对吊销延迟能容忍 1 小时的场景,比如企业内部 VPN 证书。
  3. OCSP stapling must-staple:证书里带 id-pe-tlsfeature 扩展声明必须 stapling,客户端在缺失 stapling 时拒绝握手,从而防止攻击者屏蔽 OCSP 查询。开启成本较高,主要用于公网服务。

2.5 Kubernetes 内的 mTLS 实践

在 K8s 集群里部署 mTLS 常见的三种组合:

组合 A:cert-manager + 内部 CA。适合”自己管 CA、不想上服务网格”的场景,应用显式挂载证书,手动处理 rotate reload。

组合 B:cert-manager + Let’s Encrypt。只用于出口侧(网关到外网),集群内部不用,因为 Let’s Encrypt 只签公网域名。

组合 C:Istio/Linkerd 自动 mTLS。Sidecar 拦截所有流量、自动注入 SPIFFE SVID,应用代码完全无感。对应用最友好,但引入了 sidecar 的内存、延迟、复杂度代价。

对应用层要做的事,最容易忽略的两点:

三、SPIFFE 规范

SPIFFE(Secure Production Identity Framework For Everyone)是 CNCF 的毕业项目,定义了一套跨平台的工作负载身份规范。SPIRE 是它的参考实现。

3.1 SPIFFE ID 格式

SPIFFE ID 是一个形如 URI 的字符串:

spiffe://<trust-domain>/<workload-path>

例子:

spiffe://prod.example.com/ns/payments/sa/payments-worker
spiffe://staging.example.com/service/orders
spiffe://mesh.acme.io/cluster/k8s-east/ns/billing/sa/api

SPIFFE ID 是可被另一方直接解析的身份字符串,不需要查任何数据库就能理解”我正在和谁通信”。

3.2 SVID:X.509 SVID 与 JWT SVID

SVID(SPIFFE Verifiable Identity Document)是 SPIFFE ID 的密码学载体,有两种形式:

X.509 SVID:一张 X.509 证书,SPIFFE ID 放在 Subject Alternative Name 的 URI 字段里。用于 mTLS 场景,在 TLS 握手期间双方出示证书、验证链条和 URI。

JWT SVID:一个 JWT,sub 是 SPIFFE ID,aud 是接收方的 SPIFFE ID。用于不方便建立 mTLS 的场景,比如发给 HTTP API、放在消息队列消息里、传给不支持 mTLS 的第三方。

两者的典型字段:

# X.509 SVID
Subject Alternative Name:
    URI:spiffe://prod.example.com/ns/orders/sa/orders
Validity: 1 hour

# JWT SVID (payload)
{
  "sub": "spiffe://prod.example.com/ns/orders/sa/orders",
  "aud": ["spiffe://prod.example.com/ns/payments/sa/payments"],
  "exp": 1713614400,
  "iat": 1713610800
}

3.3 Trust Bundle

Trust bundle 是”我这个信任域用哪些公钥/证书来签发 SVID”的公开清单:

{
  "spiffe_refresh_hint": 60,
  "keys": [
    {
      "use": "jwt-svid",
      "kty": "EC",
      "crv": "P-256",
      "kid": "2026Q2-signing-1",
      "x": "...",
      "y": "..."
    },
    {
      "use": "x509-svid",
      "kty": "RSA",
      "x5c": ["MIID...=="]
    }
  ]
}

Federation 时一个信任域会暴露 /bundle 端点,另一个信任域定期拉取并缓存,这样两边的工作负载就能互相验证。这一部分在跨集群、跨云的场景里非常关键,否则就要退化成”手动交换根证书”。

四、SPIRE 架构深度

SPIRE 把 SPIFFE 的规范变成可运行的系统,核心有三个组件:Server、Agent、Workload API。

4.1 SPIRE Server

Server 管理信任域的”大脑”:

一个精简的 server.conf 例子:

server {
  bind_address = "0.0.0.0"
  bind_port = "8081"
  trust_domain = "prod.example.com"
  data_dir = "/run/spire/data"
  log_level = "INFO"
  default_x509_svid_ttl = "1h"
  default_jwt_svid_ttl = "5m"
  ca_ttl = "24h"
  ca_key_type = "ec-p256"
}

plugins {
  DataStore "sql" {
    plugin_data {
      database_type = "postgres"
      connection_string = "postgresql://spire:xxx@db.internal/spire?sslmode=require"
    }
  }

  KeyManager "aws_kms" {
    plugin_data {
      region = "us-east-1"
      key_policy_file = "/etc/spire/kms-policy.json"
    }
  }

  NodeAttestor "k8s_psat" {
    plugin_data {
      clusters = {
        "prod" = {
          service_account_allow_list = ["spire:spire-agent"]
          audience = ["spire-server"]
        }
      }
    }
  }

  UpstreamAuthority "disk" {
    plugin_data {
      cert_file_path = "/etc/spire/upstream-ca.pem"
      key_file_path  = "/etc/spire/upstream-ca-key.pem"
    }
  }
}

4.2 SPIRE Agent

Agent 部署在每个节点上(K8s 里是 DaemonSet),负责两件事:

  1. Node attestation:向 Server 证明”我是什么节点,我运行在哪个环境”。K8s 下用 k8s_psat 插件,通过 projected service account token 来向 Server 证明自己运行在某个 ServiceAccount 下;AWS 下用 aws_iid,直接交 EC2 instance identity document;无法自动验证的环境用 join_token(Server 预先发一个一次性 token,Agent 用它首次注册)。

  2. Workload attestation:当一个本地进程来 Workload API 请求 SVID 时,Agent 通过 kernel 提供的 selector(UID、GID、PID、cgroup、container label、K8s Pod info)判断这是哪个工作负载,再和 Server 那边的注册条目匹配,决定下发哪个 SVID。

Agent 配置示例:

agent {
  data_dir = "/run/spire/data"
  log_level = "INFO"
  server_address = "spire-server.spire.svc.cluster.local"
  server_port = "8081"
  socket_path = "/run/spire/sockets/agent.sock"
  trust_bundle_path = "/run/spire/bundle/bundle.crt"
  trust_domain = "prod.example.com"
}

plugins {
  NodeAttestor "k8s_psat" {
    plugin_data {
      cluster = "prod"
      token_path = "/var/run/secrets/tokens/spire-agent"
    }
  }

  WorkloadAttestor "k8s" {
    plugin_data {
      kubelet_read_only_port = 0
      skip_kubelet_verification = false
      node_name_env = "MY_NODE_NAME"
    }
  }

  WorkloadAttestor "unix" {
    plugin_data {
      discover_workload_path = true
    }
  }

  KeyManager "memory" {
    plugin_data = {}
  }
}

注意 KeyManager "memory" 表示 Agent 本地密钥只放内存,进程重启后会重新申请,让私钥永远不落盘。

4.3 Workload API

Workload API 是 Agent 提供给同节点工作负载的一个 gRPC 接口,监听在一个 Unix domain socket(默认 /run/spire/sockets/agent.sock)。工作负载通过挂载这个 socket 来获取身份——不需要 token、不需要环境变量、不需要配置文件,因为 socket 本身就是 Unix 级别的鉴权(Agent 能从 socket connection 上读到对端 PID,进而做 workload attestation)。

Go 里用官方 go-spiffe 库的最短代码:

package main

import (
    "context"
    "crypto/tls"
    "log"
    "net/http"
    "time"

    "github.com/spiffe/go-spiffe/v2/spiffeid"
    "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
    "github.com/spiffe/go-spiffe/v2/workloadapi"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    source, err := workloadapi.NewX509Source(ctx,
        workloadapi.WithClientOptions(
            workloadapi.WithAddr("unix:///run/spire/sockets/agent.sock")))
    if err != nil {
        log.Fatalf("unable to create X509Source: %v", err)
    }
    defer source.Close()

    svid, err := source.GetX509SVID()
    if err != nil {
        log.Fatalf("unable to fetch SVID: %v", err)
    }
    log.Printf("my SPIFFE ID: %s", svid.ID)

    // 只允许 payments 这一个对端调用自己
    allowed := spiffeid.RequireFromString(
        "spiffe://prod.example.com/ns/payments/sa/payments")
    tlsCfg := tlsconfig.MTLSServerConfig(source, source,
        tlsconfig.AuthorizeID(allowed))

    srv := &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsCfg,
        Handler:   http.HandlerFunc(handler),
    }
    log.Fatal(srv.ListenAndServeTLS("", ""))
}

func handler(w http.ResponseWriter, r *http.Request) {
    peer := r.TLS.PeerCertificates[0]
    id, _ := spiffeid.FromURI(peer.URIs[0])
    w.Write([]byte("hello " + id.String()))
}

重点X509Source 在背后起了一个 gRPC stream 和 Agent 保持长连接,SVID 过期前 Agent 会主动推送新的证书,应用侧的 tls.Config 每次握手时都会调用 GetCertificate 取最新值——应用代码不需要关心轮换,这是 SPIFFE 最舒服的一点。

4.4 Federation

当两个信任域(例如 prod 集群和 staging 集群,或者自家集群和合作伙伴集群)需要互通时,SPIRE 提供 federation 机制:

federation {
  bundle_endpoint {
    address = "0.0.0.0"
    port = 8443
  }

  federates_with "partner.example.com" {
    bundle_endpoint_url = "https://spire.partner.example.com:8443"
    bundle_endpoint_profile "https_spiffe" {
      endpoint_spiffe_id = "spiffe://partner.example.com/spire/server"
    }
  }
}

两边周期性拉对方的 bundle,缓存到本地。注册条目可以声明 federatesWith,Agent 下发 SVID 的时候会把对应的外部 trust bundle 一起给工作负载,这样它在握手时能验证来自另一个信任域的对端。

4.5 故障模式:Agent 与 Server 断连之后

生产上最容易被问到的问题:SPIRE Server 挂了会怎样?

答案分情况:

工程上务必做的事:

  1. SPIRE Server 的 datastore 用外部 HA 数据库,绝对不要用默认的 SQLite。
  2. CA key 放 KMS/HSM,不要放磁盘。
  3. Server 本身按 region 部署多实例;所有 Agent 配 server_address 指向 LB 或者 headless service。
  4. 定期备份 datastore,演练过恢复流程。

五、云厂商 Workload Identity

自建 SPIRE 很强大但运维成本不低。如果你的工作负载已经主要跑在云上,可以借助云厂商的 workload identity,它的思路是:把 Kubernetes ServiceAccount 的 token 作为 OIDC identity token,换取一个临时的云 IAM 凭据。

5.1 AWS IRSA(IAM Roles for Service Accounts)

IRSA 在 2019 年推出,核心链路:

  1. EKS 集群启用 OIDC identity provider(API server 暴露 /.well-known/openid-configuration 和 JWKS)。
  2. 在 IAM 里把这个 provider 注册为 identity provider。
  3. 创建 IAM 角色,trust policy 指向这个 OIDC provider,并限定 sub 必须是某个 system:serviceaccount:<ns>:<sa>
  4. K8s 里给 ServiceAccount 加 annotation eks.amazonaws.com/role-arn
  5. Pod 运行时,AWS SDK 读环境变量 AWS_WEB_IDENTITY_TOKEN_FILE,拿 projected token 去调 sts:AssumeRoleWithWebIdentity,换到临时 AK/SK/Session Token。

K8s ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders
  namespace: prod
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/orders-role
    eks.amazonaws.com/sts-regional-endpoints: "true"
    eks.amazonaws.com/token-expiration: "3600"

IAM Trust Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/ABCDEF1234567890"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/ABCDEF1234567890:aud": "sts.amazonaws.com",
          "oidc.eks.us-east-1.amazonaws.com/id/ABCDEF1234567890:sub": "system:serviceaccount:prod:orders"
        }
      }
    }
  ]
}

关键点

5.2 Pod Identity(AWS 新方案)

2023 年 AWS 推出了 Pod Identity,本质是把 OIDC 这一层收到 EKS Pod Identity Agent(DaemonSet)里,应用不用再看 web identity token,直接走标准 container credential provider。配置更简单(不用管 OIDC provider、sub 条件),但底层原理和 IRSA 类似。

5.3 GCP Workload Identity

GCP 的方案叫 Workload Identity(也有个新版叫 Workload Identity Federation 面向集群外),核心是:

  1. 创建 GKE 集群时启用 Workload Identity。
  2. 在 GCP 里建一个 GCP ServiceAccount(GSA)。
  3. iam.workloadIdentityUser 角色把 GSA 绑定到某个 K8s ServiceAccount(KSA):
gcloud iam service-accounts add-iam-policy-binding \
  gsa-orders@proj.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="serviceAccount:proj.svc.id.goog[prod/orders]"
  1. 给 K8s SA 加 annotation:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders
  namespace: prod
  annotations:
    iam.gke.io/gcp-service-account: gsa-orders@proj.iam.gserviceaccount.com
  1. Pod 里调用 GCP API 时,metadata server 会返回 GSA 的 access token。

GCP 的好处是应用完全透明——SDK 默认会去 metadata.google.internal,Workload Identity 把它替换成节点上的 GKE metadata server,注入正确的 token,应用代码不改。

5.4 Azure Workload Identity

Azure 上的思路叫 federated credential:

  1. 启用 AKS OIDC issuer。
  2. 在 Azure AD 里创建 App registration 或 User-Assigned Managed Identity。
  3. 在这个 identity 上配置 federated credential,issuer 填 AKS 的 OIDC URL,subjectsystem:serviceaccount:<ns>:<sa>
  4. K8s SA 加 annotation:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders
  namespace: prod
  annotations:
    azure.workload.identity/client-id: 11111111-2222-3333-4444-555555555555
  1. Pod 加 label azure.workload.identity/use: "true",mutating webhook 会注入环境变量和 projected token。应用端 DefaultAzureCredential 自动识别。

5.5 K8s ServiceAccount Token Projection

以上三家方案的共同基础都是 K8s 的 projected service account token。默认的 automountServiceAccountToken 放出来的 token 是”和 ServiceAccount 绑定的长期 token”,不适合做 workload identity(没有 audience、没有过期、重启后还能用)。

Projected token 则解决了这些问题:

apiVersion: v1
kind: Pod
metadata:
  name: orders
spec:
  serviceAccountName: orders
  containers:
    - name: app
      image: orders:1.0
      volumeMounts:
        - name: token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: token
      projected:
        sources:
          - serviceAccountToken:
              path: aws-token
              expirationSeconds: 3600
              audience: sts.amazonaws.com

三个关键参数:

六、Service Mesh 与 SPIFFE 的集成

Service Mesh 本质上是”帮你把 mTLS 接入做到透明”的控制面,Istio 和 Linkerd 都原生基于 SPIFFE。

6.1 Istio:Citadel / istiod 如何签发 SPIFFE SVID

Istio 的控制面 istiod 里有一个 CA 组件(早期叫 Citadel,现在合并进 istiod),它会:

  1. 给每个 sidecar 签发一张 X.509 SVID,SAN 里写 spiffe://<trust-domain>/ns/<ns>/sa/<sa>(默认 trust domain 是 cluster.local)。
  2. sidecar(Envoy)启动时通过 xDS 向 istiod 推送 CSR,istiod 用 K8s ServiceAccount token 验证身份,然后签发。
  3. sidecar 把证书加载到内存,所有出站/入站流量用 mTLS 包起来。

默认 TTL 是 24h,renewBefore 12h,应用完全感知不到轮换。

6.2 PeerAuthentication 与 AuthorizationPolicy

PeerAuthentication 控制”是否要求 mTLS”:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: prod
spec:
  mtls:
    mode: STRICT

STRICT 表示不接受任何非 mTLS 流量;PERMISSIVE 两种都接受(迁移期用);DISABLE 关掉。

AuthorizationPolicy 控制”谁能调用谁”,直接按 SPIFFE ID 放行:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: orders-allow-payments
  namespace: prod
spec:
  selector:
    matchLabels:
      app: orders
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - "cluster.local/ns/prod/sa/payments"
      to:
        - operation:
            methods: ["POST"]
            paths: ["/orders"]

principals 匹配的是对端 SPIFFE ID 去掉 scheme 的部分。写的时候要注意 trust domain 的格式,默认是 cluster.local,跨集群会是 <mesh-id> 或自定义值。

6.3 Linkerd 的做法

Linkerd 的设计简化很多:控制面 identity 组件给每个 pod 的 linkerd-proxy 签发 SPIFFE SVID(trust domain 默认 <cluster>.linkerd.cluster.local),所有 mesh 内通信都是 mTLS,无法关闭。授权通过 Server/ServerAuthorization/HTTPRoute 等 CRD 做。

Linkerd 的 proxy 是用 Rust 写的 micro-proxy,内存占用只有 Envoy 的 1/10 左右,对延迟敏感的系统更友好。

七、工程坑点

以下是生产环境里真实踩过的坑,按严重程度排列。

7.1 时间不同步导致证书验不过

mTLS 握手会验证 notBeforenotAfter,两台机器时间差超过证书生效时间的窗口就会失败。曾经遇到某节点 NTP 同步失败,时间偏 5 分钟,所有 1h TTL 的证书全部验不过,流量黑洞。

对策:生产环境 NTP 必须强制、监控时钟偏移、SPIRE 服务端 TTL 不要低于 10 分钟。

7.2 Trust bundle 滚动时机错配

换 root CA 的时候,如果先下线旧 root 再上线新 root,中间一段时间没人信新签的证书;如果先上线新 root 再下线旧 root,就必须在所有客户端都更新完 bundle 之后才能切。

对策:bundle rotation 永远是”先加新 CA 到 bundle → 等所有消费者更新 → 再切换签发到新 CA → 等所有旧证书过期 → 才能从 bundle 里移除旧 CA”四阶段,每阶段留一个证书 TTL 的时间。

7.3 SDS / Workload API 连接断开

应用连 Agent 的 Unix socket,Agent 重启或者 socket 文件被删掉,应用的 gRPC stream 会报错。如果应用没有实现重连逻辑,就会在 TTL 到期时突然挂掉。

对策:go-spiffe 的 X509Source 默认带重连;自己实现 Workload API 客户端的务必加指数退避重连。Agent 重启时尽量用 livenessProbe + preStop 优雅处理。

7.4 IRSA 的 sub 条件漏写

IAM Trust Policy 里如果只写 aud 没写 sub,任何 pod 拿自己 SA 的 token 都能 assume 这个 role。这是一个很常见的权限提升漏洞。

对策:IRSA role 的 trust policy 必须同时写 audsub,并且 sub 精确到单个 system:serviceaccount:<ns>:<sa>。用条件 key 时选 StringEquals,不是 StringLike

7.5 K8s projected token 不自动 reload

应用只在启动时读了一次 token 文件,一小时之后 kubelet 已经写进了新的 token,但应用还用旧的。不同 SDK 行为不一致:AWS SDK v2 会自动刷新,部分自研客户端不会。

对策:token 文件必须 watch 变化或每次使用前读取。把它和”从磁盘读一次永不变”的 secret 区分开。

7.6 证书文件权限

0644 的私钥文件会被同节点上的其他容器读到。Pod 里虽然有命名空间隔离,但 emptyDir、hostPath、projected volume 的权限设置往往被忽略。

对策:私钥文件 defaultMode: 0400,使用专门的 non-root user,必要时用 fsGrouprunAsUser。Secret volume 也要这样处理。

7.7 应用直接信任 SAN 中的 DNS 而忽略 SPIFFE URI

一些老代码用 VerifyHostnameVerifyConnection 只看 DNS name,根本不看 URIs。这会导致”任何带对应 DNS 的证书都放行”,SPIFFE ID 形同虚设。

对策:用 go-spiffe 的 tlsconfig.AuthorizeID / AuthorizeAny,或自己在 VerifyPeerCertificate 里显式解析 URIs 并匹配 SPIFFE ID。

7.8 Istio 多 trust domain 混用

一个 mesh 默认 trust domain 是 cluster.local,两个集群直接打通会导致 SPIFFE ID 冲突(都叫 cluster.local/ns/prod/sa/orders)。

对策:多集群场景一开始就规划唯一 trust domain(例如 mesh.acme.ioeast.prod),用 meshConfig.trustDomain 设置,aliasTrustDomains 处理迁移。

7.9 JWT SVID 的 aud 校验被省略

JWT SVID 一定要校验 aud 等于自己的 SPIFFE ID,漏了就意味着任何服务拿自己的 JWT SVID 都能访问任何接口。

对策:接收方用 workloadapi.JWTSourceValidateJWTSVID(token, audience),audience 传自己的 SPIFFE ID。

7.10 监控缺失

SPIRE Agent 挂了、证书签发失败、bundle 滚动出错……这些都要有指标和告警。默认 SPIRE 暴露 Prometheus metrics,但很多团队部署完就不看了。

对策:至少监控 spire_agent_svid_rotation 失败率、spire_server_ca_manager_prepared_x509_ca 数量、spire_server_registration_entry 增减、Agent 的 workloadapi_connections

八、选型建议

没有”一招鲜”,下面按场景给建议。

8.1 只在一个云上跑、不需要跨信任域

选云原生 workload identity(AWS IRSA / GCP Workload Identity / Azure Workload Identity)。省掉 SPIRE 的运维,和云 IAM 体系无缝对接。服务间调用如果也能走云厂商的 service mesh(App Mesh、Traffic Director)或者接入 Istio/Linkerd 就更好。

8.2 多集群、多云、混合云、有合作伙伴互信

选 SPIRE(或 Istio + SPIRE)。trust domain federation 是 SPIFFE 的核心能力,云厂商方案没有统一的跨云语义。早期可以先在一个集群落,mature 后再 federate。

8.3 想走服务网格但不想管证书

Istio 或 Linkerd,开启默认 mTLS。两者都已经内置 SPIFFE,不需要你显式管 SPIRE。Linkerd 更轻量、概念更少;Istio 更全能,支持复杂的流量治理。

8.4 老系统、无法改代码、也无法装 sidecar

用 cert-manager + 内部 CA,显式挂证书。代价是应用要自己做 rotate reload,而且 workload attestation 没法做得像 SPIFFE 那么严。适合过渡期。

8.5 访问第三方 API(比如 AWS、GCP)

直接用云厂商的 workload identity,不要自己管 AK/SK。已经在用 SPIRE 的团队可以用 SPIRE 的 aws_iid / gcp_iit 插件做 attestation,然后用 spiffe-helper 把 SVID 变成 AWS Session Token。

8.6 什么时候可以还不用

团队 < 10 人、服务 < 20 个、都在一个 K8s 里:先用 K8s NetworkPolicy + projected token + ServiceAccount token review API 也能跑。但这个阶段建议就把规划做完,服务超过 50 个之后再补上 mTLS 会非常痛苦。

8.7 决策清单

按顺序问自己:

  1. 需要跨集群/跨云互信吗?→ 是,SPIRE federation。
  2. 主要跑在某一个云上?→ 用云原生 workload identity 接 IAM。
  3. 想让应用无感?→ 上 Istio / Linkerd。
  4. 有合规要求证书必须由特定 CA 签?→ SPIRE UpstreamAuthority,或 cert-manager + Vault PKI。
  5. 对性能敏感、不想要 sidecar?→ 直连 SPIRE Workload API + go-spiffe,应用自己做 mTLS。

九、一个完整的最小样例

把前面的组件拼起来,下面是一个”在 EKS 上用 SPIRE 给 orders 服务签 SVID、和 payments 互调”的最小例子。

# namespace & spire-agent ServiceAccount
apiVersion: v1
kind: Namespace
metadata:
  name: spire
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spire-agent
  namespace: spire
---
# workload ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: orders
  namespace: prod

Registration entry(通过 spire-server entry create 命令或 controller-manager CRD 注册):

kubectl exec -n spire spire-server-0 -- \
  /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://prod.example.com/ns/prod/sa/orders \
    -parentID spiffe://prod.example.com/spire/agent/k8s_psat/prod/<node-uid> \
    -selector k8s:ns:prod \
    -selector k8s:sa:orders \
    -selector k8s:container-image:registry.example.com/orders:1.2.3 \
    -ttl 3600

Pod spec:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
  namespace: prod
spec:
  replicas: 3
  selector:
    matchLabels: {app: orders}
  template:
    metadata:
      labels: {app: orders}
    spec:
      serviceAccountName: orders
      containers:
        - name: app
          image: registry.example.com/orders:1.2.3
          env:
            - name: SPIFFE_ENDPOINT_SOCKET
              value: unix:///run/spire/sockets/agent.sock
          volumeMounts:
            - name: spire-agent-socket
              mountPath: /run/spire/sockets
              readOnly: true
      volumes:
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: Directory

应用代码用上文的 go-spiffe 例子,拿到 SVID 自动处理 mTLS。

Istio 场景下就更简单,连 volumeMount 都不要,sidecar 接管一切:

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: prod
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: orders-allow-payments
  namespace: prod
spec:
  selector:
    matchLabels:
      app: orders
  action: ALLOW
  rules:
    - from:
        - source:
            principals: ["cluster.local/ns/prod/sa/payments"]

这套组合打下来,一个新服务从部署到和其他服务安全互联,不需要任何人工发证书、发 token,应用代码不用碰 TLS。

十、和其他主题的连接

十一、参考资料


上一篇风险感知认证:设备信任、异常登录与挑战升级

下一篇RBAC、ABAC、ReBAC:权限模型怎么选

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · architecture / security

【身份与访问控制工程】IAM 全景:为什么这是高价值赛道

身份与访问控制从一个登录框演进为横跨合规、运维、平台工程和安全的系统工程。本文从一家 SaaS 公司被大客户卡在 SOC 2 合规的真实触发器切入,拆解 IAM、CIAM、IGA、PAM、SSO、目录服务六个子领域的边界,分析 Okta、Entra ID、Auth0、Keycloak、Ping 等主流厂商的定位与落差,给出工程师视角的介入判据与选型路径。

2026-04-21 · architecture / security

【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO

B2B 大客户的安全团队只认自家 Okta,集团五条产品线要统一账号,合规团队要求所有入口都可审计——这些需求最终都落在同一个协议上:OpenID Connect。本文从一个真实的商业谈判场景切入,系统拆解 OIDC 在 OAuth 2.0 之上增加了什么、授权码流程的每一个字段为什么存在、企业集成里最常见的六类实现坑,以及 Discovery、JWKS、Dynamic Client Registration、mTLS Client Auth 的工程细节,帮助你把 SSO 从 demo 做到可以放进 SOC 2 审计报告里。


By .