Zero trust security mechanism in mesh
What is the purpose of the change
This is a draft commit of xDS security support. Mainly including:
- Role based Authorization framework
- mTLS support in istio mesh
Proposal (Chinese)
背景
随着微服务架构的广泛应用,服务间的安全通信与访问控制变得至关重要。Dubbo作为业界领先的RPC框架,其安全性同样不容忽视。本提案旨在构建一套全面、细致的Dubbo零信任安全体系,通过mTLS认证和鉴权机制,确保服务间通信的机密性、完整性和可访问性,从而提升整个微服务架构的安全性。

目标:为Dubbo提供一套零信任安全实现,包含数据面和可选的控制面。
- 数据面:使Dubbo框架实现安全相关xDS协议(SDS、LDS、CDS等),接收控制面下发的安全资源并进行解析,通过解析的规则配置安全性能力。
- 控制面(可选):提供一套Dubbo定制的控制面,使其更贴合Dubbo自身特性。也可直接选择使用现有的控制面作为xDS server(如istio)
实现概览
概述
mesh下的零信任安全体系,关注于两个维度的安全性
- 验证(Authentication,Authn),关注于保证通信对端服务的信息可信且未被篡改。
- 鉴权(Authorization,Authz),关注于对可信服务进行权限验证,确认其是否有访问目标资源的权限
在实践中,鉴权和验证通常在L4(传输层)和L7(应用层)进行,代表协议为TCP协议与HTTP协议。
1、传输层下的验证模型
传输层通常通过TLS/SSL加密实现验证。对端通过证书表示自身身份,被调用方通过查找信任存储库、验证信任链来确定证书是否可信。相比常见的单向TLS(一方提供证书,另一方验证证书),mutual(双向)TLS要求通信双方均提供证书并进行验证。
证书由集群内的证书签发服务签发。通常由服务发起一个包含了新生成证书的CSR请求,证书签发服务为证书签名,并确保它可以被信任链中的根证书或某个中间证书验证
信任存储由集群内的证书管理服务提供,可能与证书签发服务相同。服务通过它获取信任存储,通常是根证书和其签发的部分中间证书的集合。
当对端提供证书的签名可以被信任存储中的任一证书的公钥验证,则认为对端证书可信。
1.1 Istio的mTLS验证模型
1、istio下服务部署时,其istio agent(和envoy一样在服务pod部署)会首先生成一个证书,并向istiod:15012/IstioCertificateService 发送一条包含证书的CSR请求,为证书签名。
2、此处服务需要通过其pod下的k8s(第一方)或 istio(第三方)ServiceAccount JWT Token 向 istiod 表明身份,以建立最初的信任关系。
3、签名完成后,istio agent 作为SDS服务端,以SDS协议将证书提供给envoy,enovy 再通过该证书向 istiod:15012/AdsService 安全端口建立连接,并进行Xds协议的后续调用。
在该场景下,istio agent永远作为SDS服务端向envoy提供服务,自身则直接与istiod通信,完成证书签名、轮换等工作。
4、在与控制面的Ads服务建立安全连接后,后续服务间通过envoy进行通信时均使用该证书建立mTLS连接。
5、证书的轮换由agent周期性发送CSR请求实现。服务的证书不会在其严格到期时再轮换,而是在其快到期时就尝试签名新证书。IstioCertificateService会将最新的信任链作为CSR请求响应返回,服务则可以使用该信任链更新信任存储。
1.2 X.509 SPIFFE Identity
SPIFFE是 Secure Production Identity Framework for Everyone(适用于所有人的安全生产身份框架)的缩写。它以一串spiffe://开头的URL为服务提供身份,通常以X.509证书或JWT中附加字段的形式出现。这些spiffe的载体称为SVID。
spiffe://<trust-domain>/<namespace>/<service-account>
满足spiffe标准的X.509证书示例 (X.509 SVID):
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
04:10:df:a2:3b:45:3b:75:ec:fd:fa:41:1b:84:cb:70:d9
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=SPIFFE CA
Validity
Not Before: Mar 10 12:00:00 2021 GMT
Not After : Mar 10 12:00:00 2022 GMT
Subject: CN=spiffe://example.org/foo/service1/workload1 <--- SPIFFE URL
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Alternative Name:
URI: spiffe://example.org/foo/service1/workload1 <--- SPIFFE URL
Signature Algorithm: sha256WithRSAEncryption
5c:ca:ba:8e:92:...:00:00:00:00:00:00:00:00:00:00:00:00
其中主题和拓展字段均包含了一个SPIFFE url,表示该证书来源于信任域为
example.org、命名空间为foo,service1服务的工作负载workload1。
Istio签发的证书符合SPIFFE标准。Istio agent在生成证书时,会根据pod的环境变量创建一个SPIFFE URL并将其写入待签名的X.509证书, 并携带k8s SA Token或其它身份证明验证自身身份。
Istiod收到CSR请求时,会通过Token和注册条目校验代理为证书写入的SPIFFE信息是否合规。
通过满足SPIFFE标准的X.509证书,上游服务就可以确定下游服务的具体信息(位于哪个信任域、服务名称及工作负载),这些信息可用于后续的访问权限验证流程。
1.2.1 SPIRE
SPIRE是SPIFFE的运行时环境,它是CNCF提供的一个SPIFFE实现。Istio提供了自己的SPIRE实现(Istio CA/Citadel/Istiod +Istio pilot),并支持和CNCF的实现进行集成。
SPIRE主要由Server 和 代理组成。Server 作为中心化的服务,为客户端(服务实例)提供证书签发、管理服务,而代理则负责生成证书、管理证书并请求Server为证书签名、续签,为Node内工作负载提供有效证书。

1.2.2 注册条目
注册条目描述了哪些工作负载可以和哪些SPIFFE身份关联起来,为工作负载关联身份提供了准入条件。这些条目存储在SPIRE Server上,由SPIRE Server管理。
注册条目由选择器、SPIFFE ID、联邦关系等字段组成。
- 选择器用于确定哪些工作负载可以授予对应的SPIFFE ID,如基于UNIX用户ID、UNIX组、K8s服务账户、pod、环境变量、进程属性进行选择
- SPIFFE ID为已成功注册的ID
- 联邦关系则指定了哪些其它信任域可接收某个SPIFFE ID,跨集群安全通信依赖该配置
1.3 istio的mTLS配置
istio的mTLS配置由 PeerAuthentication 定义,用于配置是否启用mTLS及对应策略
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: "example-peer-policy"
namespace: "foo"
spec:
selector:
matchLabels:
app: reviews
mtls:
mode: STRICT
该策略包含三种类型
STRICT:严格模式,经由该服务的流量必须由mTLS加密,否则会被拒绝。这要求对端服务必须支持TLS且可以提供有效证书,一般同为istio mesh下且部署有sidecar的应用。
PERMISSIVE:宽容模式,服务接受通过mTLS或普通TCP连接的流量。此配置适用于集群中同时存在非istio服务的迁移环境。
NONE/不设置:关闭模式,服务不主动配置证书和密钥,也不会主动建立mTLS连接。适用于少数高性能、不能承受TLS带来的额外开销的场景。
mTLS策略有两种解析方式:
-
从CDS协议的Endpoint配置中显式获取TLS配置
-
从LDS协议隐式获取,通过解析 virtualInbound 监听器链中的协议配置实现:若 TransportProtocol仅包含TLS,则认为使用STRICT模式;若同时包含其它协议,则认为使用PERMISSIVE模式;若不包含TLS协议则认为不启用。
2、应用层下的验证模型
应用层下的验证一般指上游服务对对端服务身份进行验证,确保其包含的身份证明不被伪造、身份信息可信。
身份证明通常由JWT携带,并通过HTTP Header传递,通常包含在 Authorizaiton / Cookie 头部中,但也可以是用户的某个自定义Header、表单中的某个字段,并不强制。
根据服务调用者的不同,其JWT来源也可能不同,主要可分为:网格内服务通信时的服务账户JWT与网格外消费者提供的JWT。不管是哪种来源的JWT,payload及签名都必须包含可被上游服务识别,且符合当前配置鉴权及验证策略的标识。
2.1 网格内服务身份证明
在istio mesh内部,istio默认使用k8s ServiceAccount JWT作为服务的身份证明。
认证服务需要具备根据服务信息的JWT的签发能力,并支持其自动更新。对于默认的K8s SA Token,约定存储于服务pod下的 /var/run/secrets/kubernetes.io/token , 并由k8s负责其自动更新及轮换。相比证书,Token的过期时间更短,通常为几个小时。
2.2 网格外消费者身份证明
非网格或istio体系外的请求者也可以使用其它外部认证服务,如OAuth2、OIDC、或某些云服务提供商的认证服务。Istio也支持与这些服务进行集成,使用其他来源的JWT而非Sa JWT。
2.3 身份证明的验证
istio内,用户通过配置 RequestAuthentication 和 AuthorizationPolicy 来指定服务如何验证对端JWT并进行鉴权。其中,RequestAuthentication更注重于验证JWT的有效性,而AuthorizationPolicy则注重于对有效JWT进行更细粒度的RBAC鉴权。

RequestAuthentication示例:
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
name: "jwt-auth"
namespace: istio-system
spec:
selector:
matchLabels:
app: myapp
jwtRules:
- issuer: "https://example.com"
jwksUri: "https://example.com/.well-known/jwks.json"
JWT的有效性主要由其是否能被JWKS验证决定。
JWKS(JSON Web Key Set)是一组JWK集合,每个JWT包含了用于验证JWT签名的公钥信息。当一个JWT的签名可以被某个JWK的公钥验证时,认为它是可信的。
在RequestAuthentication中指定的jwksUri是一个外部的JWKS服务地址。除了配置一个独立的JWKS服务,它也可以配置为静态的JWK串。
Istio 将 RequestAuthentication 中的 JWKS 、issuer等验证属性由LDS协议下发到envoy sidecar。LDS中,它会被转换为 envoy.filters.http.jwt_authn 过滤器配置。http_authn过滤器会在下游请求入站时提取JWT,并按照定义的策略进行验证。
jwt_authn过滤器配置示例:
http_filters:
- name: envoy.filters.http.jwt_authn
config:
providers:
example_provider:
issuer: "https://example.com"
audiences:
- "example_service"
local_jwks:
inline_string: "{ \"keys\": [{ \"kty\": \"RSA\", \"kid\": \"abc\", ... }] }"
rules:
- match:
prefix: "/"
requires:
provider_name: "example_provider"
这个过滤器使用了静态JWKS。
3、应用层下的鉴权模型
istio下,基于角色的权限控制(RBAC)由AuthorizationPolicy定义。用户通过配置AuthorizationPolicy来为服务进行细粒度的权限控制,包括端口、请求PATH、方法、Header等属性。

AuthorizaitonPolicy示例:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: example-policy
namespace: default
spec:
selector:
matchLabels:
app: my-app
action: ALLOW
rules:
- to:
- operation:
paths: ["/private"]
when:
- key: request.auth.claims[iss]
values: ["https://example.com"]
- key: request.auth.principal
values: ["[email protected]"]
RBAC过滤器本身不和JWT绑定,它可以仅通过一些通用的连接属性进行权限校验。但对端服务的JWT可以为校验提供更多的元数据,以支持基于更多属性的权限控制。
在Envoy中,jwt_authn过滤器在校验JWT同时也会将各种声明提取到校验上下文(HTTP header等)中,RBAC过滤器后续可以从上下文中获取这些属性用于校验。
以上AuthorizationPolicy转换为rbac_filter的配置:
http_filters:
- name: envoy.filters.http.rbac
config:
rules:
action: ALLOW
policies:
"policy-1":
permissions:
- header:
name: ":path"
exact_match: "/private"
principals:
- authenticated:
principal_name:
exact: "[email protected]"
实现
数据流总览:
传输层验证实现
如前文所述,mesh下传输层安全性主要由mTLS提供,包括
1、证书签名前服务身份证明的获取。身份证明来源多样(k8s SA token、外部OAUTH服务,等),格式统一为 JWT。
2、通过服务运行环境生成证书、使用CSR请求为证书签名并缓存证书
3、从信任存储源拉取信任存储(证书链)
4、LDS 监听Listener配置,按port获取对端安全策略(PERMISSIVE、STRICT、NONE)
5、连接建立时提供证书,并要求对端提供证书,使用信任存储验证
Istio + mTLS保证了:
- 传输信息可靠性(不被篡改)
- 双端服务身份可信性(SPIFFE+信任链)
接口及实现:
-
ServiceIdentitySource 服务身份源,提供JWT形式的服务身份证明
@SPI(value = "default", scope = ExtensionScope.APPLICATION) public interface ServiceIdentitySource { @Adaptive(value = {"serviceIdentity"}) String getJwt(URL url); }默认实现 KubeServiceJwtIdentitySource 使用本地k8s服务账户Token
-
CertSource 证书源,提供有效的X.509证书并负责维护
@SPI(scope = ExtensionScope.FRAMEWORK) public interface CertSource { @Adaptive(value = {"mesh", "cert_source"}) CertPair getCert(URL url); } -
TrustSource 信任源,提供信任证书链(信任存储)并负责维护
@SPI(scope = ExtensionScope.FRAMEWORK) public interface TrustSource { @Adaptive(value = {"mesh","trust"}) X509CertChains getTrustCerts(URL url); }CertSource和TrustSource的默认实现 IstioCitadelCertificateSigner,通过本地服务身份生成证书并请求Istio签名,并缓存CSR请求返回的信任链作为信任存储。
-
LdsListener LDS资源监听器
public interface LdsListener extends XdsResourceListener<Listener> {}传输层鉴权下 TlsModeListener 负责解析Listener中各入站port的协议配置(是否支持TLS/SSL),将配置写入TlsModeRepo。
-
MeshCredentialProvider
现有CertProvider接口的实现,从CertSource和TrustSource获取证书及信任存储,并通过TlsModeRepo判断对端的安全策略。作为客户端时,其要求服务端提供证书;作为服务端时,其要求客户端提供证书。
应用层验证&鉴权实现
应用层验证、鉴权操作集中在Provider链路:
1、请求入站时从请求中解析出消费者请求凭据,包括所有可用于进行访问控制的字段:
- 通用连接字段,对端/代理ip:端口、目标ip:端口
- 应用层协议字段:HTTP method/path/header/form字段
- 服务身份字段:对端pod id、服务名称、cluster、namespace、ServiceAccount、SPIFFE ID、......
- 服务身份载体字段(JWT):(iss)签发者、(aud)受众、(iat)签发时间、(exp)过期时间、......
- Dubbo特有标识字段:服务接口名、目标方法名、版本、......
其中应用层协议字段协议相关,提供 CredentialFactory 从请求中解析协议相关字段
@SPI(scope = ExtensionScope.APPLICATION)
public interface CredentialFactory {
@Adaptive("protocol")
RequestCredential getRequestCredential(URL url, Invocation invocation);
}
- 默认实现TripleMeshRequestCredentialFactory,主要提供基于HTTP的协议字段解析
2、验证、鉴权规则的获取,包括:
- 凭据验证规则
- 凭据鉴权规则
若对端是具有SPIFFE身份的服务(TLS中已提供证书),则凭据的验证是可选的,支持不提供JWT/不对JWT的签名进行校验。
首先,根据当前连接的上下文信息(URL、Invocaition、请求凭据)
1、RequestAuthorizer(实现RoleBasedAuthorizer)先获取适用于当前请求的规则源(RuleSource)
2、使用对应的规则工厂(RuleFactory)生成规则树
3、通过 CredentialFactory 获取请求凭据,创建 AuthorizationRequestContext,并使用规则树评估请求,包括所有AND规则和OR规则。
为对端生成怎样的规则树由RuleFactory判断。生成的规则树可能包含两颗子树:验证树和鉴权树,分别使用请求凭据中的不同字段进行校验;也可能仅包含两颗树中的一个,取决于为对端使用怎样的验证策略。
@Activate
public class RoleBasedAuthorizer implements RequestAuthorizer {
private final RuleProvider<?> ruleProvider;
private final CredentialFactory credentialFactory;
private final RuleFactory ruleFactory;
private final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(RoleBasedAuthorizer.class);
/**
* TODO
* Cached rules
* Connection Identity -> Authorization Rules
*/
private final Map<String, List<RuleRoot>> rules = new ConcurrentHashMap<>();
public RoleBasedAuthorizer(ApplicationModel applicationModel) {
this.ruleProvider = applicationModel.getAdaptiveExtension(RuleProvider.class);
this.credentialFactory = applicationModel.getAdaptiveExtension(CredentialFactory.class);
this.ruleFactory = applicationModel.getAdaptiveExtension(RuleFactory.class);
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public void validate(Invocation invocation) throws AuthorizationException {
List rulesSources = ruleProvider.getSource(invocation.getInvoker().getUrl(), invocation);
List<RuleRoot> roots = ruleFactory.getRules(invocation.getInvoker().getUrl(),rulesSources);
List<RuleRoot> andRules = roots.stream()
.filter(root -> Relation.AND.equals(root.getRelation()))
.collect(Collectors.toList());
List<RuleRoot> orRules = roots.stream()
.filter(root -> Relation.OR.equals(root.getRelation()))
.collect(Collectors.toList());
RequestCredential requestCredential =
credentialFactory.getRequestCredential(invocation.getInvoker().getUrl(), invocation);
AuthorizationRequestContext context = new AuthorizationRequestContext(invocation, requestCredential);
boolean andRes = true;
for (RuleRoot rule : andRules) {
try {
if (!rule.evaluate(context) && rule.getAction().boolVal()) {
andRes = false;
break;
}
} catch (Exception e) {
logger.error(
"",
"",
"",
"Request authorization failed, source:" + invocation.getServiceName() + // TODO get source
", target URL:" + invocation.getInvoker().getUrl(),
e.getCause());
if (e instanceof AuthorizationException) {
throw (AuthorizationException) e;
}
throw new AuthorizationException(e);
}
}
boolean orRes = false;
for (RuleRoot rule : orRules) {
try {
orRes = rule.evaluate(context) && rule.getAction().boolVal();
if (orRes) {
break;
}
} catch (Exception e) {
logger.error(
"",
"",
"",
"Request authorization failed, source:" + invocation.getServiceName() + // TODO source
", target URL:" + invocation.getInvoker().getUrl(),
e.getCause());
if (e instanceof AuthorizationException) {
throw (AuthorizationException) e;
}
throw new AuthorizationException(e);
}
}
if (CollectionUtils.isEmpty(orRules)) {
orRes = true;
}
if (andRes && orRes) {
return;
}
throw new AuthorizationException("Request authorization failed: request credential doesn't meet rules.");
}
}
/**
* Provides rules for role-based authorization & authentication
*/
@SPI(value = "default", scope = ExtensionScope.APPLICATION)
public interface RuleProvider<T> {
@Adaptive(value = {"mesh", "authz_rule"})
List<T> getSource(URL url, Invocation invocation);
}
@SPI
public interface RuleFactory<T> {
@Adaptive({"mesh", "authz_rule"})
List<RuleRoot> getRules(URL url,List<T> ruleSource);
}
@SPI(scope = ExtensionScope.APPLICATION)
public interface CredentialFactory {
@Adaptive("protocol")
RequestCredential getRequestCredential(URL url, Invocation invocation);
}
规则树
规则树定义了规则属性之间的关系。如:
- 同名的多个属性值,匹配其中一个
- 同级的多个属性,必须匹配其中一个
- 同级的多个属性,必须匹配其中所有
- 具有多个属性结点的同级父属性结点,他们的子属性必须全部匹配/匹配其中一个
无论是Isitio的AuthorizationPolicy,又或是envoy的RBAC filter配置,都可以使用规则树表示完整的规则结构。
目前提供两套构建规则树的实现:
1、以KubeApiClient拉取AuthorizationPolicy配置,以Map构建规则树
2、以LDS监听器获取RBAC、authn过滤器配置,以HTTP filter构建规则树
public interface RuleNode {
/**
* evaluate if the request can match rules in this node and its children
*/
boolean evaluate(AuthorizationRequestContext context);
String getNodeName();
enum Relation {
AND,
OR,
NOT
}
}
public class RuleRoot extends CompositeRuleNode {
/**
* Relations between rule tree roots.
* All roots that has Relation=AND will do AND, and all roots has Relation=OR will do OR.
*/
private Action action;
public RuleRoot(Relation relation, Action action, String name) {
super(name, relation);
this.action = action;
}
public RuleRoot(Relation relation, Action action) {
super("", relation);
this.action = action;
}
public Action getAction() {
return action;
}
@Override
public boolean evaluate(AuthorizationRequestContext context) {
boolean result;
if (relation == Relation.AND) {
result = children.values().stream()
.allMatch(childList -> childList.stream().allMatch(ch -> ch.evaluate(context)));
} else {
// Relation == OR
result = children.values().stream()
.anyMatch(childList -> childList.stream().anyMatch(ch -> ch.evaluate(context)));
}
return result;
}
/**
* The action of authorization policy
*/
public enum Action {
/**
* The request must map this policy
*/
ALLOW("ALLOW", true),
/**
* The request must not map this policy
*/
DENY("DENY", false);
private final String name;
private boolean boolVal;
Action(String name, boolean boolValue) {
this.name = name;
this.boolVal = boolValue;
}
public static Action map(String name) {
name = name.toUpperCase();
switch (name) {
case "ALLOW":
return ALLOW;
case "DENY":
return DENY;
default:
return null;
}
}
public boolean boolVal() {
return boolVal;
}
}
}
@SuppressWarnings("unchecked,rawtypes")
public class LeafRuleNode implements RuleNode {
/**
* e.g rules.from.source.principles
*/
private String rulePropName;
/**
* patterns that matches required values
*/
private List<Matcher> matchers;
public LeafRuleNode(List<? extends Matcher> expectedConditions, String name) {
this.matchers = (List<Matcher>) expectedConditions;
this.rulePropName = name;
}
public LeafRuleNode(Matcher matcher, String name) {
this.matchers = Collections.singletonList(matcher);
this.rulePropName = name;
}
@Override
public boolean evaluate(AuthorizationRequestContext context) {
// If we have multiple values to validate, then every value must match at list one matcher
for (Matcher matcher : matchers) {
Object toValidate = context.getRequestCredential().getRequestProperty(matcher.propType());
if (!matcher.match(toValidate)) {
return false;
}
}
return true;
}
@Override
public String getNodeName() {
return rulePropName;
}
}
Quality Gate passed
Issues
204 New issues
0 Accepted issues
Measures
0 Security Hotspots
0.0% Coverage on New Code
0.4% Duplication on New Code
Since the target branch is dedicated for xds development, I will merge this pull request now for better cooperation.
