零信任安全架构实战——BeyondCorp 思想在微服务中的落地方案
零信任安全架构实战——BeyondCorp 思想在微服务中的落地方案
适读人群:关注企业安全架构的工程师和架构师 | 阅读时长:约20分钟 | 核心价值:理解零信任的核心思想,并在微服务架构中逐步落地
我在一家公司工作时,有段时间公司的内网安全模型很传统——只要接入了公司内网,就被认为是"可信的",服务之间的调用完全没有认证,只要在同一个 K8s namespace 里,任何服务都可以访问任何其他服务。
那时候我们有个服务出了漏洞,被注入了恶意代码。攻击者通过那个被入侵的服务,在内网里横向移动,访问了数据库服务、消息队列、配置中心……几乎所有内部服务都被遍历了一遍,因为内部没有任何访问控制。
后来做安全加固的时候,我深入研究了零信任安全(Zero Trust Security)的思想,特别是 Google 的 BeyondCorp 方案。
这篇文章讲的是零信任的核心思想,以及如何在微服务架构里逐步落地。
零信任的核心思想
传统的安全模型是"城堡护城河"模型:城墙(防火墙)把内部和外部分开,城墙内部的人互相信任,城墙外部的人被阻挡在外。
零信任的核心思想:永远不信任,永远验证(Never Trust, Always Verify)。
无论是来自内网还是外网的请求,无论是人还是服务,都需要:
- 身份认证:你是谁?
- 权限授权:你被允许做什么?
- 持续验证:你的行为是否符合预期?
零信任不是某一个具体的工具,而是一套安全设计原则。理解这一点很重要,因为很多人把零信任等同于某个具体的产品(比如"上了 Istio 就等于零信任了"),这是一种误解。Istio 是实现零信任的工具,但工具本身不等于零信任——如果你装了 Istio 但用的是 PERMISSIVE 模式(不强制 mTLS),或者没有配置 AuthorizationPolicy,你并没有获得零信任的安全保障。
为什么"内网即安全"这个假设失效了
传统的"城堡护城河"模型在互联网早期是有道理的,那时候内网确实是受到物理边界保护的受信环境。但这个假设在今天已经完全失效,原因有几个:
一是内网边界本身变得模糊。云计算、远程办公、SaaS 服务的普及,让"内网"和"外网"的界限越来越模糊。员工从家里用 VPN 接入,也算"内网";云上的服务器和本地数据中心通过专线连接,也算同一个"内网"。内网的边界在不断扩大,每一个扩大的节点都是潜在的入口。
二是内网威胁同样存在。研究表明,大量的安全事故来自内部——员工的笔记本被恶意软件感染,攻击者通过被控制的内部机器横向移动;供应链攻击通过可信的内部软件传播;内部人员的恶意行为。如果所有内网设备都被无条件信任,这些威胁就没有任何防线。
三是微服务增加了内部攻击面。单体应用时代,内部通信都在同一个进程里,没有网络攻击面。微服务架构里,服务间通信走网络,每一个服务接口都是潜在的攻击目标。这让"内部服务可以互相信任"的假设变得非常危险。
微服务的零信任落地
服务间认证:mTLS(双向 TLS)
mTLS 是微服务间零信任认证的基础。每个服务都有自己的 TLS 证书,服务间通信时双方都要验证对方的证书。
在 K8s 里,最优雅的 mTLS 方案是使用 Service Mesh(Istio 或 Linkerd)——它在应用不感知的情况下,通过 sidecar proxy 自动为所有服务间通信加上 mTLS。
Istio 的 mTLS 配置:
# 开启严格 mTLS(拒绝非 TLS 的请求)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # 严格模式:必须有 mTLS,否则拒绝# 宽松模式(迁移期用,接受 mTLS 和非 mTLS)
spec:
mtls:
mode: PERMISSIVE服务间授权:AuthorizationPolicy
有了 mTLS,你知道了"请求者是谁"(通过证书里的 service account 标识)。接下来要配置"谁可以访问谁":
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: payment-service-authz
namespace: production
spec:
selector:
matchLabels:
app: payment-service
action: ALLOW
rules:
# 只允许 order-service 调用支付接口
- from:
- source:
principals:
- "cluster.local/ns/production/sa/order-service"
to:
- operation:
paths: ["/api/v1/payments/*"]
methods: ["POST"]
# 允许 monitoring 服务访问 actuator 端点
- from:
- source:
principals:
- "cluster.local/ns/monitoring/sa/prometheus"
to:
- operation:
paths: ["/actuator/*"]
methods: ["GET"]这个 AuthorizationPolicy 的效果:
payment-service只接受来自order-service的支付请求- 其他任何服务发来的请求都会被拒绝(403 Forbidden)
- 这正是横向移动攻击发生时,能限制爆炸半径的关键
用户认证:JWT + OIDC
服务间的认证用 mTLS,用户到服务的认证用 JWT(JSON Web Token)+ OIDC(OpenID Connect)。
在 API 网关层验证 JWT,并通过请求头传递给下游服务:
# Istio RequestAuthentication:验证 JWT
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: user-jwt-auth
namespace: production
spec:
selector:
matchLabels:
app: api-gateway
jwtRules:
- issuer: "https://auth.company.com"
jwksUri: "https://auth.company.com/.well-known/jwks.json"
audiences:
- "api.company.com"
forwardOriginalToken: true # 将 JWT 传递给下游服务# 对所有进入 api-gateway 的请求强制要求 JWT
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: production
spec:
selector:
matchLabels:
app: api-gateway
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["*"] # 有效的 JWT,任何 subject 都允许
when:
- key: request.auth.claims[scope]
values: ["api:read", "api:write"] # JWT 里必须有这些 scopeSpring Boot 里的零信任实现
如果没有 Service Mesh,在 Spring Boot 应用层面实现服务间零信任认证:
使用 Spring Security + JWT 做服务间认证
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/internal/**").hasRole("SERVICE") // 内部接口只允许其他服务调用
.requestMatchers("/api/public/**").hasRole("USER") // 公开接口需要用户 token
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}服务端(被调用方)发 token 给调用方:
@Service
public class ServiceTokenProvider {
private final JwtEncoder jwtEncoder;
public String generateServiceToken(String serviceId, List<String> roles) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("https://auth.company.com")
.issuedAt(now)
.expiresAt(now.plus(Duration.ofMinutes(5))) // 短有效期
.subject(serviceId)
.claim("roles", roles)
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}调用方在请求头里带上 token:
@Configuration
public class RestClientConfig {
@Bean
public RestClient internalRestClient(ServiceTokenProvider tokenProvider) {
return RestClient.builder()
.requestInterceptor((request, body, execution) -> {
String token = tokenProvider.generateServiceToken(
"payment-service", List.of("SERVICE"));
request.getHeaders().setBearerAuth(token);
return execution.execute(request, body);
})
.build();
}
}踩坑实录
踩坑一:mTLS 证书轮换导致服务中断
Istio 的 mTLS 证书默认 24 小时自动轮换,这对大多数场景是透明的。但我们有一个服务持有长时间的连接(WebSocket),在证书轮换的那一刻,旧的 TLS 连接用的是旧证书,新连接用新证书,两种连接在同一时刻共存。
某次证书轮换时,有几十个旧连接无法完成 TLS 重协商,被强制断开,用户体验到了明显的连接中断。
解决方案:WebSocket 服务配置了更宽松的 PeerAuthentication(PERMISSIVE 模式),允许在证书轮换窗口期间同时接受新旧两种证书。Istio 1.15+ 也改进了证书轮换机制,对长连接更友好。
踩坑二:AuthorizationPolicy 的 allow-nothing 陷阱
Istio 的 AuthorizationPolicy 有一个"default deny"行为:如果一个 workload 有 AuthorizationPolicy,那么所有未被显式允许的请求都会被拒绝。
我有一次为 payment-service 添加了第一条 AuthorizationPolicy,只允许 order-service 访问。上线后发现 monitoring 的健康检查探针也被拒绝了,因为没有在 Policy 里显式允许 K8s 的 health check。
# 必须显式允许 K8s 健康检查(来自 kubelet)
- to:
- operation:
paths: ["/actuator/health"]
methods: ["GET"]
when:
- key: connection.mtls
values: ["true"]实际上 Kubelet 的健康检查不走 mTLS(它从 node 上发请求,不是服务间通信),需要特殊处理:
# 允许不带认证的健康检查(来自本机)
- to:
- operation:
paths: ["/actuator/health", "/actuator/info"]踩坑三:零信任引入了大量配置,运维成本爆炸
我们引入 Istio 之后,每个新服务上线都要配置:mTLS PeerAuthentication、服务间授权 AuthorizationPolicy、用户认证 RequestAuthentication……一个新服务上线要多配置 3-4 个 YAML 文件。
团队开始抱怨"框架太重"。
解决方案:做成自动化。用一个 K8s Operator 或者 Helm chart,当新服务创建时自动生成标准的 Istio 配置,只需要在 Deployment 上加几个 annotation:
metadata:
annotations:
security.company.com/allowed-callers: "order-service,api-gateway"
security.company.com/public-paths: "/actuator/health,/actuator/info"Operator 根据这些 annotation 自动生成 AuthorizationPolicy,开发者只需要声明业务意图,不需要了解 Istio 的细节。
深度解析:Service Mesh 之外的零信任选择
很多团队一听到"零信任",第一反应是"要上 Istio"。Istio 确实是实现微服务间零信任最完整的方案,但它不是唯一选择,也不是所有场景下的最佳选择。
Istio 的真实成本
Istio 是一个功能丰富但也相当复杂的系统。引入 Istio,意味着:
每个 Pod 都要运行一个 Envoy sidecar,内存消耗增加(每个 sidecar 约 50-100MB),在有几百个 Pod 的集群里,这是几十 GB 的额外内存。
Istio 的控制平面(istiod)需要维护所有服务的配置、证书、服务发现信息,自身的运维复杂度不低。Istio 版本升级历史上曾经出现过不兼容变更,导致需要仔细规划升级。
调试问题变得更复杂。原本一个简单的服务调用失败,现在要在应用日志、Envoy 日志、Istio 控制平面日志三个地方找原因。
这些成本不是否定 Istio 的理由,而是说在选择之前要认真评估。
Linkerd:更轻量的 Service Mesh
Linkerd 是另一个 CNCF 的 Service Mesh 项目,设计理念是"简单、轻量、专注"。它的 sidecar(linkerd-proxy)是用 Rust 写的,内存占用比 Envoy 小得多(约 10-15MB),而且功能集更聚焦,配置相对简单。
如果你的主要需求是 mTLS 和基本的流量管理,不需要 Istio 的全部功能,Linkerd 是一个很好的选择。
不用 Service Mesh 的零信任
如果不想引入 Service Mesh 的复杂性,可以在应用层面做服务间认证。
最简单的方案是 mutual TLS 在应用层实现:每个服务持有自己的客户端证书,调用其他服务时带上客户端证书,被调用方验证。这需要每个服务自己管理证书,运维成本较高。
另一种方案是基于 JWT 的服务间认证(就是本文里 Spring Security 那一节的方案)。每个服务维护自己的密钥对,服务间调用时用私钥签发 JWT,对方用公钥验证。实现相对简单,不需要 Service Mesh,适合中小规模的微服务集群。
SPIFFE(Secure Production Identity Framework For Everyone)是一个开源标准,定义了在动态基础设施里如何给服务分配身份。SPIRE 是 SPIFFE 的参考实现,可以配合各种服务间通信方案使用,包括 Envoy、gRPC、自定义方案。
我的建议
100 个 Pod 以下的集群:考虑应用层 JWT 认证,成本低,效果够用。
100-500 个 Pod 的集群:考虑 Linkerd,轻量,运维成本可控,提供 mTLS 和基本流量管理。
500 个 Pod 以上、有复杂流量管理需求的集群:Istio 或者 Cilium(基于 eBPF 的更现代方案),功能强大但需要专人维护。
深度解析:身份认证与访问控制的边界
零信任架构里有一个概念容易混淆:认证(Authentication)和授权(Authorization)的区别,以及它们在零信任中的角色。
认证解决"你是谁"的问题
mTLS 的核心作用是认证——通过证书验证,确认"这个发请求的服务确实是 order-service,而不是伪装的"。JWT 对用户的作用也是认证——验证 token 的签名,确认"这个 token 确实是我们的认证服务颁发的,代表用户 A"。
认证的强度取决于凭证的不可伪造性。密码可以被猜到,token 可以被窃取,但 mTLS 的私钥被攻破的难度要高得多(私钥不在网络上传输,而且 Istio 会定期轮换)。
授权解决"你能做什么"的问题
Istio 的 AuthorizationPolicy 负责授权——即使已经确认了"这是 order-service 发来的请求",还要判断"order-service 被允许调用 payment-service 的支付接口吗?"
零信任的核心主张是:认证通过 ≠ 授权通过。传统的内网模型里,认证通过(在内网里)就等于所有权限,这是最大的漏洞。零信任要求对每次操作都明确授权。
这个原则在实践中意味着两件事:一是授权要精确到操作级别,而不是服务级别。不是"order-service 可以访问 payment-service",而是"order-service 可以调用 payment-service 的 POST /payments 接口"。粒度越细,被攻破的服务能做的事情就越少。
二是授权策略要与服务的功能意图一致,而不只是技术上可行的所有操作。如果 order-service 根本不需要查询支付历史,就不应该给它 GET /payments/history 的权限,哪怕这样给了也"不会有什么问题"——最小权限原则不仅仅是防止攻击,也是防止因为代码 bug 导致的意外操作。
最小权限原则的实践
授权配置的原则:从"拒绝所有"出发,逐步添加必要的允许规则。不要从"允许所有"出发再逐步限制,因为很难做到"考虑到所有需要限制的情况"。
每个服务的 AuthorizationPolicy 应该只允许以下几类访问:
- 已知的服务调用者(精确到 ServiceAccount)
- 允许的接口路径(精确到 path,不要用通配符)
- K8s 基础设施的健康检查(kubelet、prometheus scraping)
定期审查 AuthorizationPolicy,清理不再有效的规则(服务已经下线,但规则还在)。规则越多,越难维护,越容易出错。
异常访问检测
认证和授权解决了"正常访问"的安全问题,但还有一类攻击是"合法凭证被盗用"——攻击者获取了合法服务的证书,发起的请求看起来完全合法。
这需要行为分析来检测:正常情况下 order-service 每分钟调用 payment-service 100 次,如果突然变成 10000 次,这是异常。通过监控服务间调用量、调用时段、调用链路,建立行为基线,偏离基线的行为触发告警。
这就是"持续验证"的含义——不只是在建立连接时验证身份,而是持续监控行为是否符合预期。
零信任落地路径
零信任不能一步到位,要分阶段推进:
Phase 1:可见性建立
- 部署 Service Mesh(PERMISSIVE 模式,不强制 mTLS)
- 观察服务调用关系,建立服务依赖图
- 不改变现有的访问控制
Phase 2:加固关键服务
- 对最敏感的服务(数据库层、支付服务)先开启 STRICT mTLS
- 配置明确的 AuthorizationPolicy,只允许已知调用方
- 解决因为访问控制收紧引起的问题
Phase 3:全面推广
- 所有服务开启 STRICT mTLS
- 建立标准化的 AuthorizationPolicy 管理流程
- 接入 SIEM(安全信息和事件管理),持续监控异常访问
总结
零信任不是一个可以"安装"的产品,它是一套需要持续推进的安全文化和实践。
最核心的改变是心态:把内网当外网对待。任何服务都不应该因为"在内网"就被无条件信任。
对于大多数中小团队,从 Service Mesh 的 mTLS 和 AuthorizationPolicy 开始是最有效的第一步,它不需要改应用代码,却能大幅提升服务间访问控制的安全性。
