[WIP] [OSPP] [2.2.x] mtls support for 2.2.x branch (#3466)

* feat: Add mtls related support
pull/3449/head^2
Zing-20 1 year ago committed by GitHub
parent 50f37bf670
commit 181ff019f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,4 +5,6 @@
<suppressions>
<suppress files="[\\/]spring-cloud-alibaba-examples[\\/]" checks="HideUtilityClassConstructorCheck" />
<suppress files=".*" checks="LineLength" />
<suppress files="[\\/]build[\\/]generated[\\/]source[\\/]proto" checks=".*"/>
<suppress files="[\\/]target[\\/]generated-sources[\\/]protobuf" checks=".*"/>
</suppressions>

@ -257,6 +257,12 @@
<version>${spring.context.support.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
<version>${revision}</version>
</dependency>
<!-- Testing Dependencies -->
</dependencies>

@ -9,7 +9,7 @@ image::pic/resource-transform.png[]
Microservices Governance的规则转换模块会将不同控制面下发的规则进行统一的转换目前支持将来自IstioOpenSergo等控制面下发的规则统一转换为Spring Cloud Alibaba统一抽象出的数据结构以供后续使用
如果在项目中使用Istio来实现规则转换首先注意需要搭建一个Kubernetes集群并且在其中部署Istio具体参考 https://istio.io/latest/zh/docs/setup/install[Istio安装]。然后在需要接收到来自Istio规则的应用(一般为服务消费者)中添加如下starter依赖
如果在项目中使用 `Istio` 来实现规则转换首先注意需要搭建一个Kubernetes集群并且在其中部署Istio具体参考 https://istio.io/latest/zh/docs/setup/install[Istio安装]。然后在需要接收到来自Istio规则的应用中添加如下starter依赖
[source,xml,indent=0]
----
<dependency>
@ -17,9 +17,43 @@ Microservices Governance的规则转换模块会将不同控制面下发的规
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
----
OpenSergo请参考 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/label-routing-example[Spring Cloud Alibaba Routing Examples]
OpenSergo请参考 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/label-routing-example[Spring Cloud Alibaba Routing Examples]
之后在application.yml配置文件中设置如下配置内容
其中,连接 `Istio` 控制面有如下两种手段:
==== 注入pilot-agent(推荐)
用户可以通过注入 `pilot-agent` 作为 `xds` 协议的代理,以轻松地对接到 `Istio` 控制平面只需要在Spring Cloud Alibaba应用所在pod上加上如下 `annotation`
[source,yaml,indent=0]
----
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
----
并且在 `application.yml` 中启用 `pilot-agent`
[source,yaml,indent=0]
----
server:
port: ${SERVER_PORT:80}
spring:
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
----
==== 直连Istio控制面
用户如果想要本地调试治理模块的代码,则可以不注入 `pilot-agent` 直连 `Istio` 控制面。若不注入 `pilot-agent` 则用户需要在 `application.yml` 配置文件中设置如下配置内容:
[source,yaml,indent=0]
----
@ -35,10 +69,11 @@ spring:
enabled: ${ISTIO_CONFIG_ENABLE:true}
host: ${ISTIOD_ADDR:127.0.0.1}
port: ${ISTIOD_PORT:15010}
polling-pool-size: ${POLLING_POOL_SIZE:10}
polling-time: ${POLLING_TIMEOUT:10}
pod-name: ${POD_NAME:auth-5ffd65b7b8-l8tpf}
namespace-name: ${NAMESPACE_NAME:default}
istiod-token: ${ISTIOD_TOKEN:}
log-xds: ${LOG_XDS:true}
use-agent: ${USE_AGENT:false}
----
各字段的含义如下:
@ -47,20 +82,42 @@ spring:
|是否开启鉴权| spring.cloud.governance.auth.enabled|true|
|是否连接Istio获取鉴权配置| spring.cloud.istio.config.enabled|true|
|Istiod的地址| spring.cloud.istio.config.host|127.0.0.1|
|Istiod的端口| spring.cloud.istio.config.port|15012|注连接15010端口无需TLS连接15012端口需TLS认证
|应用从Istio拉取配置的线程池大小| spring.cloud.istio.config.polling-pool-size|10|
|应用从Istio拉取配置的间隔时间| spring.cloud.istio.config.polling-time|30|单位为秒
|连接Istio 15012端口时使用的JWT token| spring.cloud.istio.config.istiod-token|应用所在pod的 `/var/run/secrets/tokens/istio-token` 文件的内容|
|Istiod的端口| spring.cloud.istio.config.port|15012|注连接15010端口无需TLS连接15012端口需认证
|应用所处k8s pod名| spring.cloud.istio.config.pod-name|POD_NAME环境变量的值|
|应用所处k8s namespace| spring.cloud.istio.config.namespace-name|NAMESPACE_NAME环境变量的值|
|连接Istio 15012端口时使用的JWT token| spring.cloud.istio.config.istiod-token|应用所在pod的 `/var/run/secrets/tokens/istio-token` 文件的内容(暂时只支持third-part jwt, 后续也会支持first-part jwt)|
|是否打印xDS相关日志| spring.cloud.istio.config.log-xds|true|
|是否注入pilot-agent| spring.cloud.istio.config.use-agent|false|
|===
其中若要在直连istio控制面时连接istio控制面的15012端口需要将此应用的 `serviceaccount` 作为 `projected volumn` 挂载到k8s的 `/var/run/secrets/tokens/istio-token` 路径上,具体操作如下:
注意应用运行在K8s环境中在非默认命名空间下的应用需要接收Istiod下发的规则需要将运行的应用K8s的元信息注入以下环境变量中具体操作方式可参考 https://kubernetes.io/zh-cn/docs/tasks/inject-data-application/environment-variable-expose-pod-information[Kubernetes文档]:
1.为Spring Cloud Alibaba应用所在pod声明一个 `projected volumn`
|===
|环境变量名|K8s pod metadata name
|POD_NAME|metadata.name
|NAMESPACE_NAME|metadata.namespace
|===
[source,yaml,indent=0]
----
spec:
volumes:
- name: istio-token
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: istio-ca
expirationSeconds: 43200
path: istio-token
----
2.将这个 `projected volumn` 挂载到 `/var/run/secrets/tokens/istio-token` 路径上:
[source,yaml,indent=0]
----
spec:
containers:
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: istio-token
----
在本地进行调试时,如果您想模拟其他工作负载的身份,可以在 `application.yml` 的 `istio-token` 字段中直接填入其他工作负载的token。
=== 路由

@ -0,0 +1,157 @@
== Spring Cloud Alibaba Mtls
=== 零信任安全
==== 介绍
零信任Zero Trust是一种设计安全防护架构的方法它的核心思路是默认情况下所有交互都是不可信的。这与传统的架构相反后者可能会根据通信是否始于防火墙内部来判断是否可信。零信任架构则摒弃了这种隐式的信任模型强调在每个交互环节都进行验证和授权。
具体而言,零信任力求弥合依赖隐式信任模型和一次性身份验证的安全防护架构之间的缺口。在零信任架构中,所有用户、设备和应用程序都被视为潜在的威胁,需要进行身份验证和授权,无论它们是否位于内部网络。零信任实施以身份为中心的访问控制机制,基于“持续验证+动态授权”的模式构筑安全基石。
==== Istio如何实现零信任安全
image::pic/istio-security.png[width=50%,align=center]
如上图所示Istio提供两种类型的认证
1Request Authentication
用于终端用户认证以验证附加到请求的凭据。这种认证方式用于验证终端用户的身份以便在服务中进行更细粒度的访问控制和授权。Istio 可以集成多种用户认证机制如基于JSON Web Token (JWT) 的认证、OAuth 2.0、OpenID Connect等。
2Peer Authentication
用于服务到服务的认证以验证建立连接的客户端。使用传输层安全Transport Layer SecurityTLS协议为服务之间的通信提供加密和身份验证。每个服务都有一个独特的服务账户并使用数字证书进行身份验证。这种认证机制确保只有经过身份验证的服务才能相互通信防止未经授权的服务接入。
Spring Cloud Alibaba已经实现了RequestAuthentication和鉴权的内容参考 https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc[文档:Spring Cloud Alibaba Governance]及 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/authentication-example[示例:Spring Cloud Alibaba Authorization Examples]因此本文档主要介绍PeerAuthentication相关的内容。
Istio提供了双向TLS作为PeerAuthentication的全栈解决方案其包括
1. 为每个服务提供强大的身份,表示其角色,以实现跨群集和云的互操作性,参考 https://istio.io/latest/zh/docs/concepts/security/#istio-identity[Istio身份]。在Kubernetes平台上可以使用Kubernetes Service Account标识工作负载的身份
2. 保护服务到服务的通信;
3. 提供密钥管理系统,以自动进行密钥和证书的生成,分发和轮换,参考 https://istio.io/latest/zh/docs/concepts/security/#PKI[身份和证书管理]。
=== Spring Cloud Alibaba Mtls模块简介
Spring Cloud Alibaba Mtls 关注服务到服务的认证通过添加spring-cloud-starter-alibaba-governance-mtls模块就可以为 Spring Cloud Alibaba 应用实现双向TLS能力。
如下图所示spring-cloud-starter-alibaba-governance-mtls模块提供了证书管理、流量模式切换的功能支持 AuthorizationPolicy principals 授权策略配置,并为多种客户端/服务器实现提供了使用双向TLS交互的能力。
image::pic/mtls-architecture.png[width=60%,align=center]
==== 证书管理
===== 证书获取
首先注意需要搭建一个Kubernetes集群并且在其中部署Istio具体参考 https://istio.io/latest/zh/docs/setup/install[Istio安装]。然后在需要实现mtls能力的应用中添加如下starter依赖
[source,xml,indent=0]
----
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
----
连接 Istio 控制面有直连、注入pilot-agent两种方式参考 https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc[Spring Cloud Alibaba Governance]。
在 `application.yml` 配置文件中添加如下内容:
[source,yaml,indent=0]
----
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
----
字段含义如下:
|===
|配置项|key|默认值
|是否开启mtls| spring.cloud.mtls.enabled|true
|是否以tls流量模式启动| spring.cloud.server-tls.enabled|true
|===
====== 直连Istio控制面
image::pic/connect-directly-to-Istiod.png[width=50%,align=center]
对于Proxyless模式的Spring Cloud Alibaba应用无需使用envoy proxySpring Cloud Alibaba的SDK可以直接扮演 istio-agent 的角色直接在SDK里为此应用生成私钥以及向Istio控制面申请证书。
要在直连istio控制面时连接istio控制面的15012端口需要将此应用的 Service Account 作为 projected volumn 挂载到k8s的 /var/run/secrets/tokens/istio-token 路径上,具体操作见 https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc[Spring Cloud Alibaba Governance]。
====== 注入pilot-agent
image::pic/pilot-agent-as-agent.svg[width=70%,align=center]
如上图所示,参考 https://istio.io/latest/blog/2021/proxyless-grpc/[Istio / gRPC Proxyless Service Mesh] 的实现方式,可以将 pilot-agent 作为xDS协议的统一代理在添加 inject.istio.io/templates: grpc-agent 注解之后Spring Cloud Alibaba应用将会获取到 pilot-agent 生成的bootstrap文件文件中将会保存证书相关的路径以及证书过期时间。
===== 证书轮转
spring-cloud-starter-alibaba-governance-mtls模块会定期更新证书保证证书的有效性用户不需要手动操作。
==== 流量模式切换
Spring Cloud Alibaba Mtls模块支持通过Actuator端点赋予应用动态切换http/https模式的能力。在应用中添加如下依赖
[source,xml,indent=0]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
----
并且在 `application.yml` 中配置对外暴露Actuator端点
[source,yaml,indent=0]
----
management:
endpoints:
web:
exposure:
include: "*"
----
配置后即可通过消费Actuator端点实现http/https模式的切换具体参考 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples]。
==== AuthorizationPolicy授权策略
Istio通过授权策略对服务器端的入站流量实施访问控制参考 https://istio.io/latest/zh/docs/concepts/security/#authorization-policies[authorization-policies]。
spring-cloud-starter-alibaba-governance-auth模块为应用提供了多种鉴权能力具体可参考参考 https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc[Spring Cloud Alibaba Governance]。使用Spring Cloud Alibaba服务鉴权功能需要添加如下依赖
[source,xml,indent=0]
----
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
----
并且在 `application.yml` 中启用鉴权:
[source,yaml,indent=0]
----
spring:
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
----
在确保引入了spring-cloud-starter-alibaba-governance-mtls模块并启用TLS流量模式后接下来即可在Spring Cloud Alibaba应用所在pod上应用具体的授权策略配置具体可参考 https://istio.io/latest/zh/docs/reference/config/security/authorization-policy/[istio / authorization-policy] 及示例 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples]。
==== 框架适配
===== 服务端
针对Spring MVC(tomcat)以及Spring Webflux(Netty)提供了istio证书的自动加载以及热更新。
===== 客户端
针对feignresttemplate等客户端的实现提供了具有热更新能力的ssl上下文用户配置后可自动进行istio证书的更新。
具体示例可参考 https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples]。

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

@ -10,7 +10,7 @@ image::pic/resource-transform.png[]
The resource-transform adapter of Microservices Governance will uniformly transform the rules published by different control planes, like Istio and OperSergo, into the unified abstract data structure of Spring Cloud Alibaba for subsequent use.
If you use Istio in your project to transform the rule, first of all, note that you need to build a Kubernetes cluster and deploy Istio in it. For details, refer to https://istio.io/latest/zh/docs/setup/install[Istio installation], you need to add a following starter dependency application(Generally added to service consumer application):
If you use Istio in your project to transform the rule, first of all, note that you need to build a Kubernetes cluster and deploy Istio in it. For details, refer to https://istio.io/latest/zh/docs/setup/install[Istio installation], you need to add a following starter dependency application:
[source,xml,indent=0]
----
<dependency>
@ -18,9 +18,43 @@ If you use Istio in your project to transform the rule, first of all, note that
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
----
If you want to use OpenSergo, you can refer to https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/label-routing-example[Spring Cloud Alibaba Routing Examples]
If you want to use OpenSergo, you can refer to https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/label-routing-example[Spring Cloud Alibaba Routing Examples].
After that, configure the following configuration in the application.yml:
There are two ways to connect to `Istio` control plane:
==== Inject pilot-agent(recommend)
Users can easily connect to the `Istio` control plane by injecting pilot-agent as a proxy for the xds protocol, simply by adding the following annotation to the pod of Spring Cloud Alibaba workload:
[source,yaml,indent=0]
----
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'
----
And enable `pilot-agent` in `application.yml`:
[source,yaml,indent=0]
----
server:
port: ${SERVER_PORT:80}
spring:
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
----
==== Connect to istio control plane directly
Users who want to debug the code of the governance module locally can directly connect to the `Istio` control plane without injecting the pilot-agent. If `pilot-agent` is not injected, the user needs to set the following configuration contents in the `application.yml` configuration file:
[source,yaml,indent=0]
----
@ -36,10 +70,11 @@ spring:
enabled: ${ISTIO_CONFIG_ENABLE:true}
host: ${ISTIOD_ADDR:127.0.0.1}
port: ${ISTIOD_PORT:15010}
polling-pool-size: ${POLLING_POOL_SIZE:10}
polling-time: ${POLLING_TIMEOUT:10}
pod-name: ${POD_NAME:auth-5ffd65b7b8-l8tpf}
namespace-name: ${NAMESPACE_NAME:default}
istiod-token: ${ISTIOD_TOKEN:}
log-xds: ${LOG_XDS:true}
use-agent: ${USE_AGENT:false}
----
Here's an explanation of each field:
@ -49,19 +84,40 @@ Here's an explanation of each field:
|Whether to connect to Istio to obtain authentication configuration| spring.cloud.istio.config.enabled|true|
|Host of Istiod| spring.cloud.istio.config.host|127.0.0.1|
|Port of Istiod| spring.cloud.istio.config.port|15012|15010 port does not need TLSbut 15012 does
|Thread pool size for application to pull the config| spring.cloud.istio.config.polling-pool-size|10|
|Time interval for application to pull the config| spring.cloud.istio.config.polling-time|30|The unit is second
|JWT token for application to connect to 15012 port| spring.cloud.istio.config.istiod-token|Content of file `/var/run/secrets/tokens/istio-token` in the pod of application|
|Pod name| spring.cloud.istio.config.pod-name|value of environment variable POD_NAME|
|Namespace name| spring.cloud.istio.config.namespace-name|value of environment variable NAMESPACE_NAME|
|JWT token for application to connect to 15012 port| spring.cloud.istio.config.istiod-token|Content of file `/var/run/secrets/tokens/istio-token` in the pod of application.(Only support third-part-jwt now, first-part-jwt will be supported later)|
|Whether to print logs about xDS| spring.cloud.istio.config.log-xds|true|
|Whether to inject pilot-agent| spring.cloud.istio.config.use-agent|false|
|===
Note that the application runs in the K8s environment, and the application in the non-default namespace needs to receive the rules issued by Istiod, and needs to inject the meta information of the running application Kubernetes into the following environment variables. For the specific operation method, please refer to https://kubernetes.io/zh-cn/docs/tasks/inject-data-application/environment-variable-expose-pod-information[Kubernetes documentation]
If you want to connect to port 15012 of the `Istio` control plane when directly connecting the istio control plane, you need to apply `serviceaccount` as `projected volumn` mount to k8s path `/ var/run/secrets/tokens/istio-token`:
|===
|Environment variable name|K8s pod metadata name
|POD_NAME|metadata.name
|NAMESPACE_NAME|metadata.namespace
|===
1.Apply a `projected volumn` for the pod of Spring Cloud Alibaba workload:
[source,yaml,indent=0]
----
spec:
volumes:
- name: istio-token
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: istio-ca
expirationSeconds: 43200
path: istio-token
----
2.Mount the `projected volumn` to path `/var/run/secrets/tokens/istio-token`:
[source,yaml,indent=0]
----
spec:
containers:
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: istio-token
----
If you want to emulate the identity of other workloads when debugging locally, you can fill the `istio-token` field of `application.yml` with the tokens of other workloads.
=== Routing
==== Component support description

@ -0,0 +1,158 @@
== Spring Cloud Alibaba Mtls
=== Zero Trust Security
==== Introduction
Zero Trust is an approach to designing security protection architectures that centers on the idea that, by default, all interactions are untrustworthy. This is in contrast to traditional architectures, which may judge trustworthiness based on whether or not the communication begins inside the firewall. Zero-trust architectures, on the other hand, eschew this implicit trust model and emphasize authentication and authorization at every interaction.
Specifically, zero trust seeks to bridge the gap between security protection architectures that rely on implicit trust models and one-time authentication. In a zero-trust architecture, all users, devices and applications are considered potential threats and require authentication and authorization, regardless of whether they are located on the internal network. Zero Trust implements identity-centric access control mechanisms and builds a security foundation based on a "continuous authentication + dynamic authorization" model.
==== How Istio achieves zero-trust security
image::pic/istio-security.png[width=50%,align=center]
As shown above, Istio offers two types of authentication:
1Request Authentication
Used for end-user authentication to verify the credential attached to the request. This type of authentication is used to verify the identity of end-users for finer-grained access control and authorization in services. Istio can integrate with a variety of user authentication mechanisms, such as JSON Web Token (JWT)-based authentication, OAuth 2.0, OpenID Connect, and more.
2Peer Authentication
Used for service-to-service authentication to verify the client making the connection. Uses the Transport Layer Security (TLS) protocol to provide encryption and authentication for communication between services. Each service has a unique service account and is authenticated using digital certificates. This authentication mechanism ensures that only authenticated services can communicate with each other, preventing unauthorized access to services.
Spring Cloud Alibaba has implemented RequestAuthentication and Authentication (refer to https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc[Spring Cloud Alibaba Governance] and https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/governance-example/authentication-example[Example: Spring Cloud Alibaba Authorization Examples]). This document therefore focuses on PeerAuthentication.
Istio offers mutual TLS as a full stack solution for PeerAuthentication, which includes:
1. Provides each service with a strong identity representing its role to enable interoperability across clusters and clouds, refer to https://istio.io/latest/docs/concepts/security/#istio-identity[Istio Identity]. On Kubernetes, workloads can be identified using Kubernetes Service Account;
2. Secures service-to-service communication;
3. Provides a key management system to automate key and certificate generation, distribution, and rotation, refer to https://istio.io/latest/docs/concepts/security/#pki[Identity and certificate management].
=== Spring Cloud Alibaba Mtls Module Introduction
Spring Cloud Alibaba Mtls focuses on service-to-service authentication. By adding the spring-cloud-starter-alibaba-governance-mtls module, you can implement mutual TLS capabilities for Spring Cloud Alibaba applications.
As shown in the figure below, the spring-cloud-starter-alibaba-governance-mtls module provides certificate management, traffic pattern switching, support for AuthorizationPolicy principals configuration, and the ability to use mutual TLS interactions for a variety of client/server implementations.
image::pic/mtls-architecture.png[width=60%,align=center]
==== Certificate Management
===== Certificate Acquisition
First note that you need to build a Kubernetes cluster and deploy Istio in it, refer to https://istio.io/latest/docs/setup/install[Istio Installation]. Then add the following starter dependency to the application that needs to implement the mtls capability:
[source,xml,indent=0]
----
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
----
There are two ways to connect to the Istio control plane: direct connection and injection of pilot-agent, refer to https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc[Spring Cloud Alibaba Governance].
Add the following to the `application.yml` configuration file:
[source,yaml,indent=0]
----
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
----
The meaning of the fields is as follows:
|===
|Configuration Item|key|Default Value
|Whether to enable mtls| spring.cloud.mtls.enabled|true
|Whether to start in tls traffic mode| spring.cloud.server-tls.enabled|true
|===
====== Connect to istio control plane directly
image::pic/connect-directly-to-Istiod.png[width=50%,align=center]
For Spring Cloud Alibaba applications in Proxyless mode, there is no need to use an envoy proxy. The Spring Cloud Alibaba SDK can directly play the role of an istio-agent, generating the private key for the application and requesting a certificate from the Istio control plane directly in the SDK.
To connect to port 15012 of the istio control plane when connecting directly to the istio control plane, you need to mount this application's Service Account as a projected volumn on the /var/run/secrets/tokens/istio-token path of k8s, as described at https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc[Spring Cloud Alibaba Governance].
====== Inject pilot-agent
image::pic/pilot-agent-as-agent.svg[width=70%,align=center]
As shown in the above figure, referring to the implementation of https://istio.io/latest/blog/2021/proxyless-grpc/[Istio / gRPC Proxyless Service Mesh], pilot-agent can be used as a unified proxy for the xDS protocol, after adding the `inject.istio.io/templates: grpc-agent` annotation, the Spring Cloud Alibaba application will fetch the bootstrap file generated by the pilot-agent, which will store the path of the certificate and the certificate expiration time.
===== Certificate Rotation
The spring-cloud-starter-alibaba-governance-mtls module will update the certificate periodically to ensure the validity of the certificate, and users do not need to do it manually.
==== Traffic Mode Switching
The Spring Cloud Alibaba Mtls module enables applications to dynamically switch between http/https modes via the Actuator endpoint. Add the following dependency to your application:
[source,xml,indent=0]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
----
And configure the external exposure of the Actuator endpoint in `application.yml`:
[source,yaml,indent=0]
----
management:
endpoints:
web:
exposure:
include: "*"
----
Once configured, http/https mode switching can be achieved by consuming Actuator endpoints, refer to https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples].
==== AuthorizationPolicy
Istio enforces access control on server-side inbound traffic through authorization policies, refer to https://istio.io/latest/docs/concepts/security/#authorization-policies[authorization-policies]。
The spring-cloud-starter-alibaba-governance-auth module provides a variety of authentication capabilities for the application, see the reference at https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc[Spring Cloud Alibaba Governance]. To use the Spring Cloud Alibaba service authentication feature, you need to add the following dependency:
[source,xml,indent=0]
----
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
----
And enable authentication in `application.yml`:
[source,yaml,indent=0]
----
spring:
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
----
After ensuring that the spring-cloud-starter-alibaba-governance-mtls module has been introduced and TLS traffic mode have been enabled, you can then apply specific authorization policy configurations on the pod where the Spring Cloud Alibaba application resides, refer to https://istio.io/latest/docs/reference/config/security/authorization-policy/[istio / authorization-policy] and https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples].
==== Framework Adaptation
===== Server
Provides automatic loading and hot updating of istio certificates for Spring MVC (tomcat), and Spring Webflux (Netty).
===== Client
For client implementations such as feign, resttemplate, etc., ssl contexts with hot update capability are provided, and istio certificate updates can be performed automatically after user configuration.
Specific examples can be found in https://github.com/alibaba/spring-cloud-alibaba/tree/2.2.x/spring-cloud-alibaba-examples/mtls-example[Spring Cloud Alibaba Mtls Examples].

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

@ -29,8 +29,8 @@
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
```
2. 在应用的 `src/main/resources/application.yml` 配置文件中配置Istio相关元数据:
2. 参照[文档](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc),实现与`Istio`控制面的对接
并在`application.yml`中打开鉴权开关:
```yml
server:
port: ${SERVER_PORT:80}
@ -39,35 +39,7 @@ spring:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
host: ${ISTIOD_ADDR:127.0.0.1}
port: ${ISTIOD_PORT:15010}
polling-pool-size: ${POLLING_POOL_SIZE:10}
polling-time: ${POLLING_TIMEOUT:10}
istiod-token: ${ISTIOD_TOKEN:}
log-xds: ${LOG_XDS:true}
```
下面解释一下各字段的含义:
|配置项|key|默认值|说明
|--|--|--|--|
|是否开启鉴权| spring.cloud.governance.auth.enabled|true|
|是否连接Istio获取鉴权配置| spring.cloud.istio.config.enabled|true|
|Istiod的地址| spring.cloud.istio.config.host|127.0.0.1|
|Istiod的端口| spring.cloud.istio.config.port|15012|注连接15010端口无需TLS连接15012端口需TLS认证|
|SCA去Istio拉取配置的线程池大小| spring.cloud.istio.config.polling-pool-size|10|
|SCA去Istio拉取配置的间隔时间| spring.cloud.istio.config.polling-time|30|单位为秒
|连接Istio<br>15012端口时使用的JWT token| spring.cloud.istio.config.istiod-token|应用所在pod的`/var/run/secrets/tokens/istio-token`文件的内容|
|是否打印xDS相关日志| spring.cloud.istio.config.log-xds|true|
### 运行应用
注意应用运行在K8s环境中在非默认命名空间下的应用需要接收Istiod下发的规则需要将运行的应用K8s的元信息注入以下环境变量中具体操作方式可参考 [Kubernetes文档](https://kubernetes.io/zh-cn/docs/tasks/inject-data-application/environment-variable-expose-pod-information)
|环境变量名|K8s pod metadata name|
|--|--|
|POD_NAME|metadata.name|
|NAMESPACE_NAME|metadata.namespace|
**注您部署的应用所在的pod不需要被Istio执行自动注入因为SCA的各个治理模块将会被用来替代Envoy Proxy的各种功能。**
```
### 效果演示
下面给出几个简单的鉴权规则配置的示例:
#### IP黑白名单

@ -29,44 +29,24 @@ Before launching the example for demonstration, let's look at how a Spring Cloud
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
```
2. Configure Istio related metadata in the `src/main/resources/application` yml configuration file:
2. Connect to `Istio` control plane according to [doc](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc),
and set default route algorithm in `application.yml`.
```yml
server:
port: ${SERVER_PORT:80}
port: ${SERVER_PORT:80}
spring:
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
host: ${ISTIOD_ADDR:127.0.0.1}
port: ${ISTIOD_PORT:15010}
polling-pool-size: ${POLLING_POOL_SIZE:10}
polling-time: ${POLLING_TIMEOUT:10}
istiod-token: ${ISTIOD_TOKEN:}
log-xds: ${LOG_XDS:true}
```
Here's an explanation of each field:
|Configuration Item|key|Default Value|Description
|--|--|--|--|
|Whether to enable authentication| spring.cloud.governance.auth.enabled|true|
|Whether to connect to Istio to obtain authentication configuration| spring.cloud.istio.config.enabled|true|
|Host of Istiod| spring.cloud.istio.config.host|127.0.0.1|
|Port of Istiod| spring.cloud.istio.config.port|15012|15010 port does not need TLSbut 15012 does
|Thread pool size for application to pull the config| spring.cloud.istio.config.polling-pool-size|10|
|Time interval for application to pull the config| spring.cloud.istio.config.polling-time|30|The unit is second|
|JWT token for application to connect to 15012 port| spring.cloud.istio.config.istiod-token|Content of file `/var/run/secrets/tokens/istio-token` in the pod of application|
|Whether to print logs about xDS| spring.cloud.istio.config.log-xds|true|
cloud:
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
```
### Run the application
Note that the application runs in the K8s environment, and the application in the non-default namespace needs to receive the rules issued by Istiod, and needs to inject the meta information of the running application Kubernetes into the following environment variables. For the specific operation method, please refer to [Kubernetes documentation](https://kubernetes.io/zh-cn/docs/tasks/inject-data-application/environment-variable-expose-pod-information):
|Environment variable name|K8s pod metadata name|
|--|--|
|POD_NAME|metadata.name|
|NAMESPACE_NAME|metadata.namespace|
|POD_NAMESPACE|metadata.namespace|
**HINTThe POD in which your deployed application does not need to be automatically injected by Istio because the various governance modules of SCA will be used to replace the functions of the Envoy Proxy.**
### Demostration
@ -79,7 +59,7 @@ apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: from-ip-allow
namespace: ${namespace_name}
namespace: ${pod_namespace}
spec:
selector:
matchLabels:
@ -108,7 +88,7 @@ It indicates that the request has been authenticated by application and some met
After that, we delete the authentication rule for the IP Blocks:
```shell
kubectl delete AuthorizationPolicy from-ip-allow -n ${namespace_name}
kubectl delete AuthorizationPolicy from-ip-allow -n ${pod_namespace}
```
Then request the auth interface of this demo again, we can find that the application will return the following message because the authentication rule has been deleted:
```
@ -123,7 +103,7 @@ apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: http-headers-allow
namespace: ${namespace_name}
namespace: ${pod_namespace}
spec:
selector:
matchLabels:
@ -155,7 +135,7 @@ Auth failed, please check the request and auth rule
After that, we remove the rule for requests header authentication:
```shell
kubectl delete AuthorizationPolicy http-headers-allow -n ${namespace_name}
kubectl delete AuthorizationPolicy http-headers-allow -n ${pod_namespace}
```
Then request the auth interface of this demo again, we can find that the application will return the following message because the authentication rule has been deleted:
```
@ -170,7 +150,7 @@ apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-jwks-uri
namespace: ${namespace_name}
namespace: ${pod_namespace}
spec:
selector:
matchLabels:
@ -200,7 +180,7 @@ Auth failed, please check the request and auth rule
```
After that, we remove the rule for JWT authentication:
```shell
kubectl delete RequestAuthentication jwt-jwks-uri -n ${namespace_name}
kubectl delete RequestAuthentication jwt-jwks-uri -n ${pod_namespace}
```
Then request the auth interface of this demo again, we can find that the application will return the following message because the authentication rule has been deleted:
```

@ -6,5 +6,4 @@ metadata:
name: auth-mvc-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}
POLLING_TIME: {{ .Values.defaultPollingTime | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}

@ -6,5 +6,4 @@ metadata:
name: auth-webflux-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}
POLLING_TIME: {{ .Values.defaultPollingTime | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}

@ -6,5 +6,4 @@ metadata:
name: istio-consumer-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}
POLLING_TIME: {{ .Values.defaultPollingTime | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}

@ -5,7 +5,4 @@ metadata:
app: nacos-server
name: nacos-server-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}
POLLING_TIME: {{ .Values.defaultPollingTime | quote }}
MODE: "standalone"

@ -17,5 +17,4 @@ image:
nacosServer:
repository: nacos/nacos-server
tag: v2.1.0
istiodAddr: istiod.istio-system.svc.cluster.local
defaultPollingTime: 60
istiodAddr: istiod.istio-system.svc.cluster.local

@ -166,7 +166,7 @@ public void getDataFromControlPlaneTest() {
- [VirtualService](https://istio.io/latest/zh/docs/reference/config/networking/virtual-service/)
- [DestinationRule](https://istio.io/latest/zh/docs/reference/config/networking/destination-rule/)
### 配置
1. 首先修改pom.xml 文件,引入`spring-cloud-starter-alibaba-governance-routing`依赖。同时引入Spring Cloud Alibaba的`spring-cloud-starter-xds-adapter`模块
1. 首先修改pom.xml 文件,引入`spring-cloud-starter-alibaba-governance-routing`依赖。同时引入Spring Cloud Alibaba的`spring-cloud-starter-xds-adapter`模块
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
@ -177,42 +177,16 @@ public void getDataFromControlPlaneTest() {
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
```
2. 在`src/main/resources/application.yml`配置文件中配置Istio控制面的相关信息:
```YAML
2. 参照[文档](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/governance.adoc),实现与`Istio`控制面的对接
并在`application.yml`中配置默认路由规则:
```yml
server:
port: 18084
port: ${SERVER_PORT:80}
spring:
main:
allow-bean-definition-overriding: true
application:
name: service-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
fail-fast: true
username: nacos
password: nacos
governance:
auth:
# 是否开启鉴权
enabled: ${ISTIO_AUTH_ENABLE:false}
istio:
config:
# 是否开启Istio配置转换
enabled: ${ISTIO_CONFIG_ENABLE:true}
# Istiod ip
host: ${ISTIOD_ADDR:127.0.0.1}
# Istiod 端口
port: ${ISTIOD_PORT:15010}
# 轮询Istio线程池大小
polling-pool-size: ${POLLING_POOL_SIZE:10}
# 轮询Istio时间间隔
polling-time: ${POLLING_TIME:10}
# Istiod鉴权token(访问Istiod 15012端口时可用)
istiod-token: ${ISTIOD_TOKEN:}
# 是否打印xds相关日志
log-xds: ${LOG_XDS:true}
routing:
rule: ${ROUTING_RULE:RandomRule}
```
### 应用启动
启动三个模块的启动类分别为IstioConsumerApplication两个ProviderApplication将其注入到Nacos注册中心中。
@ -291,7 +265,7 @@ kubectl delete DestinationRule my-destination-rule
## 集成OpenSergo
**注意 本章节只是为了便于您理解接入方式,本示例代码中已经完成接入工作,您无需再进行修改。**
1. 首先修改pom.xml 文件,引入`spring-cloud-starter-alibaba-governance-routing`依赖。同时引入Spring Cloud Alibaba的`spring-cloud-starter-opensergo-adapter`模块
1. 首先修改pom.xml 文件,引入`spring-cloud-starter-alibaba-governance-routing`依赖。同时引入Spring Cloud Alibaba的`spring-cloud-starter-opensergo-adapter`模块
```XML
<dependency>
<groupId>com.alibaba.cloud</groupId>
@ -302,7 +276,7 @@ kubectl delete DestinationRule my-destination-rule
<artifactId>spring-cloud-starter-opensergo-adapter</artifactId>
</dependency>
```
2. 在application.properties配置文件中配置OpenSergo控制面的相关信息
2. 在application.properties配置文件中配置OpenSergo控制面的相关信息
```
# OpenSergo 控制面 endpoint
spring.cloud.opensergo.endpoint=127.0.0.1:10246
@ -312,7 +286,7 @@ spring.cloud.opensergo.endpoint=127.0.0.1:10246
### 下发配置
[启动 OpenSergo 控制面](https://opensergo.io/zh-cn/docs/quick-start/opensergo-control-plane/) ,并通过 OpenSergo 控制面下发流量路由规则
[启动 OpenSergo 控制面](https://opensergo.io/zh-cn/docs/quick-start/opensergo-control-plane/) ,并通过 OpenSergo 控制面下发流量路由规则
```YAML
kubectl apply -f - << EOF
@ -347,23 +321,23 @@ EOF
这条[TrafficRouter](https://github.com/opensergo/opensergo-specification/blob/main/specification/zh-Hans/traffic-routing.md) 指定了一条最简单的流量路由规则将请求头tag为v2的HTTP请求路由到v2版本其余的流量都路由到v1版本。
如果v2版本没有对应的节点则将流量fallback至v1版本。
### 效果演示
发送一条不带请求头的HTTP请求至OpenSergoConsumerApplication
发送一条不带请求头的HTTP请求至OpenSergoConsumerApplication
```
curl --location --request GET '127.0.0.1:18083/router-test'
```
因为请求头不为v2所以请求将会被路由到v1版本返回如下
因为请求头不为v2所以请求将会被路由到v1版本返回如下
```
Route in 30.221.132.228: 18081,version is v1.
```
之后发送一条请求头tag为v2的HTTP请求
之后发送一条请求头tag为v2的HTTP请求
```
curl --location --request GET '127.0.0.1:18083/router-test' --header 'tag: v2'
```
因为满足路由规则所以请求会被路由至v2版本
因为满足路由规则所以请求会被路由至v2版本
```
Route in 30.221.132.228: 18082,version is v2.
```
停止v2版本的ProviderApplication后继续发送一条请求头tag为v2的HTTP请求
停止v2版本的ProviderApplication后继续发送一条请求头tag为v2的HTTP请求
```
curl --location --request GET '127.0.0.1:18083/router-test' --header 'tag: v2'
```

@ -167,7 +167,7 @@ Please refer to [install](https://istio.io/latest/zh/docs/setup/install/) chapte
### Introduction to Istio traffic control rules
- [Istio Authorization Overview](https://istio.io/latest/zh/docs/concepts/security/#authorization)
- [Istio Security Detail](https://istio.io/latest/zh/docs/reference/config/security/)
1. First, modify the pom.xml file to introduce the `spring-cloud-starter-alibaba-governance-routing` and `spring-cloud-starter-xds-adapter` dependency
1. First, modify the pom.xml file to introduce the `spring-cloud-starter-alibaba-governance-routing` and `spring-cloud-starter-xds-adapter` dependency:
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
@ -178,42 +178,16 @@ Please refer to [install](https://istio.io/latest/zh/docs/setup/install/) chapte
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
```
2. Configure application.yml for Istio control plane:
```YAML
2. Connect to `Istio` control plane according to [doc](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/governance.adoc),
and enable auth in `application.yml`:
```yml
server:
port: 18084
port: ${SERVER_PORT:80}
spring:
main:
allow-bean-definition-overriding: true
application:
name: service-consumer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
fail-fast: true
username: nacos
password: nacos
governance:
auth:
# Is authentication enabled
enabled: ${ISTIO_AUTH_ENABLE:false}
istio:
config:
# Is Istio resource transform enabled
enabled: ${ISTIO_CONFIG_ENABLE:true}
# Istiod ip
host: ${ISTIOD_ADDR:127.0.0.1}
# Istiod port
port: ${ISTIOD_PORT:15010}
# Istiod thread-pool size
polling-pool-size: ${POLLING_POOL_SIZE:10}
# Istiod polling gap
polling-time: ${POLLING_TIME:10}
# Istiod token(For Istio 15012 port)
istiod-token: ${ISTIOD_TOKEN:}
# Whether to print xds log
log-xds: ${LOG_XDS:true}
enabled: ${ISTIO_AUTH_ENABLE:true}
```
### Startup Application
Start IstioConsumerApplication and two ProviderApplications, and inject it into the Nacos registry center.
@ -293,7 +267,7 @@ After the rule is deleted, the routing policy is not determined by whether the r
## Integrating OpenSergo
**Note that this section is only for your convenience in understanding the access method. The access work has been completed in this sample code, and you do not need to modify it.**
### Configuration
1. First, modify the `pom.xml` file to introduce the `spring-cloud-starter-alibaba-governance-routing` and `spring-cloud-starter-opensergo-adapter` dependency
1. First, modify the `pom.xml` file to introduce the `spring-cloud-starter-alibaba-governance-routing` and `spring-cloud-starter-opensergo-adapter` dependency:
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
@ -304,7 +278,7 @@ After the rule is deleted, the routing policy is not determined by whether the r
<artifactId>spring-cloud-starter-opensergo-adapter</artifactId>
</dependency>
```
2. Configure `application.yml` for OpenSergo control plane
2. Configure `application.yml` for OpenSergo control plane:
```
# The endpoint of OpenSergo ControlPlane
spring.cloud.opensergo.endpoint=127.0.0.1:10246
@ -312,7 +286,7 @@ spring.cloud.opensergo.endpoint=127.0.0.1:10246
### Startup Application
Start OpenSergoConsumerApplication and two ProviderApplications, and inject it into the Nacos registry center.
### Publish Configuration
[First start OpenSergo control plan](https://opensergo.io/docs/quick-start/opensergo-control-plane/) , Then we publish the label routing rules through the OpenSergo control plane. We publish a TrafficRouter rule.
[First start OpenSergo control plan](https://opensergo.io/docs/quick-start/opensergo-control-plane/) , Then we publish the label routing rules through the OpenSergo control plane. We publish a TrafficRouter rule:
```YAML
kubectl apply -f - << EOF
apiVersion: traffic.opensergo.io/v1alpha1
@ -346,11 +320,11 @@ EOF
This [TrafficRouter](https://github.com/opensergo/opensergo-specification/blob/main/specification/en/traffic-routing.md) specifies the simplest label routing rule. HTTP requests with a v2 header are routed to v2, and the rest of the traffic is routed to v1.
If the version v2 does not have a corresponding instance, the HTTP request will fall back to the version v1.
### Demonstrate effect
We send an HTTP request without a request header to OpenSergoConsumerApplication
We send an HTTP request without a request header to OpenSergoConsumerApplication:
```
curl --location --request GET '127.0.0.1:18083/router-test'
```
Since the request header is not v2, the request will be routed to version v1 with the following result
Since the request header is not v2, the request will be routed to version v1 with the following result:
```
Route in 30.221.132.228: 18081,version is v1.
```
@ -358,15 +332,15 @@ We then send an HTTP request with a v2 tag in its header and the request path is
```
curl --location --request GET '127.0.0.1:18083/router-test' --header 'tag: v2'
```
The request is routed to version v2 because the routing rule is matched by the request.
The request is routed to version v2 because the routing rule is matched by the request:
```
Route in 30.221.132.228: 18082,version is v2.
```
After we stop the ProviderApplication of the version v2, we send an HTTP request with the request header tag v2.
After we stop the ProviderApplication of the version v2, we send an HTTP request with the request header tag v2:
```
curl --location --request GET '127.0.0.1:18083/router-test' --header 'tag: v2'
```
because the version v2 does not have a corresponding instance, so the Http requesr is fallback to the version v1.
because the version v2 does not have a corresponding instance, so the Http requesr is fallback to the version v1:
```
Route in 30.221.132.228: 18081,version is v1.
```

@ -0,0 +1,5 @@
apiVersion: v1
appVersion: '1.0'
description: Spring Cloud Alibaba Mtls Example
name: mtls-example
version: 1.0.0

@ -0,0 +1,11 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: mtls-mvc
name: mtls-mvc-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacosServerAddr | quote }}
USE_AGENT: {{ .Values.useAgent | quote }}

@ -0,0 +1,49 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
account: sa-mtls-mvc
name: sa-mtls-mvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-mvc
spec:
replicas: 1
selector:
matchLabels:
app: mtls-mvc
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
labels:
appName: mtls-mvc
app: mtls-mvc
spec:
serviceAccountName: sa-mtls-mvc
containers:
- name: mtls-mvc
image: '{{ .Values.image.mtlsMvc.repository }}:{{ .Values.image.mtlsMvc.tag }}'
imagePullPolicy: Always
ports:
- name: http-port
containerPort: 8080
envFrom:
- configMapRef:
name: mtls-mvc-env
---
apiVersion: v1
kind: Service
metadata:
name: mtls-mvc
labels:
app: mtls-mvc
spec:
type: ClusterIP
ports:
- port: 8080
name: http-server
selector:
app: mtls-mvc

@ -0,0 +1,11 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: mtls-openfeign
name: mtls-openfeign-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacosServerAddr | quote }}
USE_AGENT: {{ .Values.useAgent | quote }}

@ -0,0 +1,49 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
account: sa-mtls-openfeign
name: sa-mtls-openfeign
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-openfeign
spec:
replicas: 1
selector:
matchLabels:
app: mtls-openfeign
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
labels:
appName: mtls-openfeign
app: mtls-openfeign
spec:
serviceAccountName: sa-mtls-openfeign
containers:
- name: mtls-openfeign
image: '{{ .Values.image.mtlsOpenfeign.repository }}:{{ .Values.image.mtlsOpenfeign.tag }}'
imagePullPolicy: Always
ports:
- name: http-port
containerPort: 8080
envFrom:
- configMapRef:
name: mtls-openfeign-env
---
apiVersion: v1
kind: Service
metadata:
name: mtls-openfeign
labels:
app: mtls-openfeign
spec:
type: ClusterIP
ports:
- port: 8080
name: http-server
selector:
app: mtls-openfeign

@ -0,0 +1,11 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: mtls-rest
name: mtls-rest-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacosServerAddr | quote }}
USE_AGENT: {{ .Values.useAgent | quote }}

@ -0,0 +1,49 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
account: sa-mtls-test
name: sa-mtls-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-rest
spec:
replicas: 1
selector:
matchLabels:
app: mtls-rest
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
labels:
appName: mtls-rest
app: mtls-rest
spec:
serviceAccountName: sa-mtls-test
containers:
- name: mtls-rest
image: '{{ .Values.image.mtlsRest.repository }}:{{ .Values.image.mtlsRest.tag }}'
imagePullPolicy: Always
ports:
- name: http-port
containerPort: 8080
envFrom:
- configMapRef:
name: mtls-rest-env
---
apiVersion: v1
kind: Service
metadata:
name: mtls-rest
labels:
app: mtls-rest
spec:
type: ClusterIP
ports:
- port: 8080
name: http-server
selector:
app: mtls-rest

@ -0,0 +1,10 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: mtls-webclient
name: mtls-webclient-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacosServerAddr | quote }}
USE_AGENT: {{ .Values.useAgent | quote }}

@ -0,0 +1,49 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
account: sa-mtls-webclient
name: sa-mtls-webclient
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-webclient
spec:
replicas: 1
selector:
matchLabels:
app: mtls-webclient
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
labels:
appName: mtls-webclient
app: mtls-webclient
spec:
serviceAccountName: sa-mtls-webclient
containers:
- name: mtls-webclient
image: '{{ .Values.image.mtlsWebClient.repository }}:{{ .Values.image.mtlsWebClient.tag }}'
imagePullPolicy: Always
ports:
- name: http-port
containerPort: 8080
envFrom:
- configMapRef:
name: mtls-webclient-env
---
apiVersion: v1
kind: Service
metadata:
name: mtls-webclient
labels:
app: mtls-webclient
spec:
type: ClusterIP
ports:
- port: 8080
name: http-server
selector:
app: mtls-webclient

@ -0,0 +1,10 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: mtls-webflux
name: mtls-webflux-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
NACOS_SERVER_ADDR: {{ .Values.nacosServerAddr | quote }}
USE_AGENT: {{ .Values.useAgent | quote }}

@ -0,0 +1,49 @@
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
account: sa-mtls-webflux
name: sa-mtls-webflux
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-webflux
spec:
replicas: 1
selector:
matchLabels:
app: mtls-webflux
template:
metadata:
annotations:
inject.istio.io/templates: grpc-agent
labels:
appName: mtls-webflux
app: mtls-webflux
spec:
serviceAccountName: sa-mtls-webflux
containers:
- name: mtls-webflux
image: '{{ .Values.image.mtlsWebflux.repository }}:{{ .Values.image.mtlsWebflux.tag }}'
imagePullPolicy: Always
ports:
- name: http-port
containerPort: 8080
envFrom:
- configMapRef:
name: mtls-webflux-env
---
apiVersion: v1
kind: Service
metadata:
name: mtls-webflux
labels:
app: mtls-webflux
spec:
type: ClusterIP
ports:
- port: 8080
name: http-server
selector:
app: mtls-webflux

@ -0,0 +1,11 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: nacos-server
name: nacos-server-env
data:
NAMESPACE_NAME: {{ .Release.Namespace | quote }}
ISTIOD_ADDR: {{ .Values.istiodAddr | quote }}
POLLING_TIME: {{ .Values.defaultPollingTime | quote }}
MODE: "standalone"

@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nacos-server
spec:
replicas: 1
selector:
matchLabels:
app: nacos-server
template:
metadata:
labels:
appName: nacos-server
app: nacos-server
spec:
containers:
- name: nacos-server
image: '{{ .Values.image.nacosServer.repository }}:{{ .Values.image.nacosServer.tag }}'
imagePullPolicy: Always
ports:
- containerPort: 8848
name: "nacos-8848"
- containerPort: 9848
name: "nacos-9848"
- containerPort: 9849
name: "nacos-9849"
envFrom:
- configMapRef:
name: nacos-server-env
---
apiVersion: v1
kind: Service
metadata:
name: nacos-server
labels:
app: nacos-server
spec:
type: ClusterIP
ports:
- port: 8848
name: "nacos-8848"
- port: 9848
name: "nacos-9848"
- port: 9849
name: "nacos-9849"
selector:
app: nacos-server

@ -0,0 +1,21 @@
image:
mtlsMvc:
repository: registry.cn-shenzhen.aliyuncs.com/sca-demo/mtls-mvc-example
tag: latest
mtlsOpenfeign:
repository: registry.cn-shenzhen.aliyuncs.com/sca-demo/mtls-openfeign-example
tag: latest
mtlsRest:
repository: registry.cn-shenzhen.aliyuncs.com/sca-demo/mtls-resttemplate-example
tag: latest
mtlsWebClient:
repository: registry.cn-shenzhen.aliyuncs.com/sca-demo/mtls-webclient-example
tag: latest
mtlsWebflux:
repository: registry.cn-shenzhen.aliyuncs.com/sca-demo/mtls-webflux-example
tag: latest
nacosServer:
repository: nacos/nacos-server
tag: v2.1.0
nacosServerAddr: nacos-server:8848
useAgent: true

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mtls-openfeign-example</artifactId>
<name>Spring Cloud Starter Alibaba Mtls Example - OpenFeign</name>
<description>Example demonstrating how to use mtls on openfeign</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,33 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class MtlsOpenfeignApplication {
public static void main(String[] args) {
SpringApplication.run(MtlsOpenfeignApplication.class, args);
}
}

@ -0,0 +1,47 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import com.alibaba.cloud.examples.feignclient.MvcClient;
import com.alibaba.cloud.examples.feignclient.WebfluxClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OpenfeignTestController {
@Resource
private MvcClient mvcClient;
@Resource
private WebfluxClient webfluxClient;
@GetMapping("/openfeign/getMvc")
public String getB(HttpServletRequest httpServletRequest) {
return mvcClient.getMvc();
}
@GetMapping("/openfeign/getWebflux")
public String getC(HttpServletRequest httpServletRequest) {
return webfluxClient.getWebflux();
}
}

@ -0,0 +1,48 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples.config;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import feign.Client;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignClientConfiguration {
@Autowired
MtlsClientSSLContext mtlsClientSSLContext;
@Bean
public Client feignClient(CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(
new feign.okhttp.OkHttpClient(new OkHttpClient.Builder()
.sslSocketFactory(mtlsClientSSLContext.getSslSocketFactory(),
mtlsClientSSLContext.getTrustManager().orElse(null))
.hostnameVerifier(mtlsClientSSLContext.getHostnameVerifier())
.build()),
lbClientFactory, clientFactory);
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples.feignclient;
import com.alibaba.cloud.examples.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "https://mtls-mvc-example",
configuration = FeignClientConfiguration.class)
public interface MvcClient {
@GetMapping("/mvc/get")
String getMvc();
}

@ -0,0 +1,31 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples.feignclient;
import com.alibaba.cloud.examples.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "https://mtls-webflux-example",
configuration = FeignClientConfiguration.class)
public interface WebfluxClient {
@GetMapping("/webflux/get")
String getWebflux();
}

@ -0,0 +1,26 @@
management:
endpoints:
web:
exposure:
include: "*"
server:
port: ${SERVER_PORT:8444}
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: true
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
application:
name: mtls-openfeign-example

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mtls-resttemplate-example</artifactId>
<name>Spring Cloud Starter Alibaba Mtls Example - RestTemplate</name>
<description>Example demonstrating how to use mtls on resttemplate</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,29 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MtlsResttemplateApplication {
public static void main(String[] args) {
SpringApplication.run(MtlsResttemplateApplication.class, args);
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ResttemplateTestController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/resttemplate/getMvc")
public String getMvc(HttpServletRequest httpServletRequest) {
return restTemplate.getForObject("https://mtls-mvc-example/mvc/get",
String.class);
}
@GetMapping("/resttemplate/getWebflux")
public String getWebflux(HttpServletRequest httpServletRequest) {
return restTemplate.getForObject("https://mtls-webflux-example/webflux/get",
String.class);
}
}

@ -0,0 +1,49 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples.config;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Autowired
MtlsClientSSLContext mtlsClientSSLContext;
@Bean
@LoadBalanced
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate
.setRequestFactory(new HttpComponentsClientHttpRequestFactory(
HttpClientBuilder.create()
.setSSLContext(mtlsClientSSLContext.getSslContext())
.setSSLHostnameVerifier(
mtlsClientSSLContext.getHostnameVerifier())
.build()));
return restTemplate;
}
}

@ -0,0 +1,26 @@
management:
endpoints:
web:
exposure:
include: "*"
server:
port: ${SERVER_PORT:8111}
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: true
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
application:
name: mtls-resttemplate-example

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mtls-webclient-example</artifactId>
<name>Spring Cloud Starter Alibaba Mtls Example - WebClient</name>
<description>Example demonstrating how to use mtls on webclient</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-boot-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,60 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
@SpringBootApplication
public class MtlsWebclientApplication {
public static void main(String[] args) {
SpringApplication.run(MtlsWebclientApplication.class, args);
}
@RestController
public class Controller {
@Autowired
MtlsClientSSLContext mtlsClientSSLContext;
@Autowired
private WebClient.Builder builder;
@GetMapping("/webclient/getMvc")
public Mono<String> getMvc(ServerWebExchange serverWebExchange) {
return builder.build().get().uri("https://mtls-mvc-example/mvc/get")
.retrieve().bodyToMono(String.class);
}
@GetMapping("/webclient/getWebflux")
public Mono<String> getWebflux(ServerWebExchange serverWebExchange) {
return builder.build().get().uri("https://mtls-webflux-example/webflux/get")
.retrieve().bodyToMono(String.class);
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples.config;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.http.client.HttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Autowired
MtlsClientSSLContext mtlsClientSSLContext;
@Bean
@LoadBalanced
public WebClient.Builder webClient() {
SslContext nettySslContext = null;
try {
nettySslContext = SslContextBuilder.forClient()
.trustManager(
mtlsClientSSLContext.getTrustManagerFactory().orElse(null))
.keyManager(mtlsClientSSLContext.getKeyManagerFactory().orElse(null))
.build();
}
catch (SSLException e) {
throw new RuntimeException("Error setting SSL context for WebClient", e);
}
SslContext finalNettySslContext = nettySslContext;
HttpClient httpClient = HttpClient.create().secure(spec -> spec
.sslContext(finalNettySslContext).handlerConfigurator(sslHandler -> {
SSLEngine engine = sslHandler.engine();
SSLParameters sslParameters = engine.getSSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("");
engine.setSSLParameters(sslParameters);
}));
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
return WebClient.builder().clientConnector(connector);
}
}

@ -0,0 +1,26 @@
management:
endpoints:
web:
exposure:
include: "*"
server:
port: ${SERVER_PORT:8555}
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: true
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
application:
name: mtls-webclient-example

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mtls-mvc-example</artifactId>
<name>Spring Cloud Starter Alibaba Mtls Example - MVC</name>
<description>Example demonstrating how to use mtls on tomcat server</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,53 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import java.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class MtlsMvcApplication {
public static void main(String[] args) {
SpringApplication.run(MtlsMvcApplication.class, args);
}
@RestController
public class Controller {
@GetMapping("/mvc/get")
public String get(HttpServletRequest httpServletRequest) {
X509Certificate[] certs = (X509Certificate[]) httpServletRequest
.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null) {
for (int i = 0; i < certs.length; i++) {
System.out.println(
"client certificate_" + i + ": \n" + certs[i].toString());
}
}
return "mvc-server received request from client";
}
}
}

@ -0,0 +1,26 @@
management:
endpoints:
web:
exposure:
include: "*"
server:
port: ${SERVER_PORT:8222}
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: true
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
application:
name: mtls-mvc-example

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<version>${revision}</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mtls-webflux-example</artifactId>
<name>Spring Cloud Starter Alibaba Mtls Example - WebFlux</name>
<description>Example demonstrating how to use mtls on netty server</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,54 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.examples;
import java.security.cert.X509Certificate;
import reactor.core.publisher.Mono;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
@SpringBootApplication
public class MtlsWebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(MtlsWebfluxApplication.class, args);
}
@RestController
public class WebFluxController {
@GetMapping("/webflux/get")
public Mono<String> get(ServerWebExchange exchange) {
X509Certificate[] certs = exchange.getRequest().getSslInfo()
.getPeerCertificates();
if (certs != null) {
for (int i = 0; i < certs.length; i++) {
System.out.println(
"client certificate_" + i + ": \n" + certs[i].toString());
}
}
return Mono.just("webflux-server received request from client");
}
}
}

@ -0,0 +1,26 @@
management:
endpoints:
web:
exposure:
include: "*"
server:
port: ${SERVER_PORT:8333}
spring:
cloud:
mtls:
config:
enabled: ${MTLS_ENABLE:true}
server-tls: ${SERVER_TLS:true}
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
enabled: true
governance:
auth:
enabled: ${ISTIO_AUTH_ENABLE:true}
istio:
config:
enabled: ${ISTIO_CONFIG_ENABLE:true}
use-agent: ${USE_AGENT:true}
application:
name: mtls-webflux-example

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

@ -0,0 +1,325 @@
# Mtls Example
## 项目说明
本项目演示如何利用 Istio 下发的证书实现Spring Cloud Alibaba下文简称SCA应用之间的双向TLS能力。目前对于服务端支持Spring MVC以及Spring WebFlux应用的适配并针对feignresttemplate等客户端的实现提供了具有热更新能力的ssl上下文配置后可自动进行istio证书的更新。
## 准备
### 安装K8s环境
请参考K8s的[安装工具](https://kubernetes.io/zh-cn/docs/tasks/tools/)小节。
### 安装Helm
请参考Helm[[安装指南]](https://helm.sh/docs/intro/install/)。
### 在K8s上安装并启用Istio
请参考Istio官方文档的[安装](https://istio.io/latest/zh/docs/setup/install/)小节。
### 应用部署
通过以下命令在K8s中部署示例项目mtls-example
```shell
helm install mtls-demo .
```
部署成功后查看pod信息如下
![pods](pic/pods.png)
## 示例
### 如何接入
在启动示例进行演示之前先了解一下应用如何接入Istio并及加载证书。 注意本章节只是为了便于理解接入方式,本示例代码中已经完成接入工作,您无需再进行修改。
1. 修改`pom.xml`文件引入Istio规则adapter、mtls模块、governance-auth模块以及actuator依赖
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```
2. 参照[文档](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc-zh/mtls.adoc),实现与`Istio`控制面的对接以及 ServiceAccount 的配置
### 快速入门
#### 查看证书
将示例应用部署至K8s并配置好应用的 ServiceAccount 后在应用所在pod内通过如下命令可查看为当前应用下发的证书
```shell
openssl s_client -connect 127.0.0.1:${port} </dev/null 2>/dev/null | openssl x509 -inform pem -text
```
以SpringMVC(Tomcat)服务器为例,可以看到 spiffe 编码的 ServiceAccount 内容来作为 Subject Alternative Name
```
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6a:e4:29:15:41:94:75:67:3c:7f:85:26:ff:37:3d:55
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = cluster.local
Validity
Not Before: Oct 20 06:55:54 2023 GMT
Not After : Oct 21 06:57:54 2023 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:d3:a8:31:78:82:d3:fb:a4:01:2e:02:c3:70:86:
fb:f4:e3:f8:42:fc:4e:ce:97:d0:5a:6a:b9:1b:eb:
fd:2c:ad:16:4c:8a:12:a0:23:fe:ea:3c:7d:3f:3f:
6b:b5:6c:c5:d3:fe:f5:09:68:b9:03:c7:1d:08:ef:
96:e3:1a:60:b2:5e:9c:c6:ff:13:42:14:08:0c:fc:
66:28:98:65:d2:49:ef:9f:d8:c3:be:70:12:12:24:
d6:25:5d:99:eb:63:4e:19:51:13:c9:09:73:79:b8:
ac:07:ee:a2:99:75:62:bb:f6:32:48:da:fb:69:32:
cc:cf:f1:7e:ef:e8:dd:8f:88:b2:c3:cb:73:b0:ed:
a6:60:e2:3c:e1:b2:7e:f4:9d:ad:1c:be:9d:ba:1b:
1a:e4:0c:a8:a7:a6:ea:f5:f6:96:47:74:77:bd:fa:
4a:5f:bd:05:9b:8b:e7:d5:49:27:22:30:5b:79:e2:
62:a8:71:60:36:8a:a6:79:41:76:0a:af:db:8f:fb:
e4:f3:fa:19:3c:bd:49:48:39:96:7f:16:24:0e:c6:
0d:20:01:9c:70:32:cd:1d:92:32:e0:b3:72:bb:1a:
78:ac:49:b2:ff:90:36:4d:d0:d0:e8:5c:02:05:7c:
c7:b9:a8:99:72:0e:48:8c:f5:41:7d:ce:8d:92:73:
2c:3b
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:9D:4E:93:61:65:5B:47:8B:E5:B4:1D:6C:A7:06:9E:35:AD:9B:8A:0B
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/default/sa/mtls-resttemplate-example
Signature Algorithm: sha256WithRSAEncryption
6f:ad:f8:d2:e1:1e:45:99:63:9e:0a:6c:06:28:14:fb:7f:f6:
6d:c2:4c:e6:2f:43:28:34:5e:a2:16:88:0d:1c:fb:80:d3:bd:
72:7e:cc:63:4a:91:54:33:e9:0d:bb:bc:92:46:13:68:0a:9f:
83:25:9c:0d:8a:cd:8f:41:d4:f3:c5:b6:48:39:04:da:9e:7a:
51:00:3c:34:15:c5:20:c9:00:d1:ac:53:4e:3a:6c:68:a6:6c:
8a:1d:c0:e6:13:06:45:56:13:d5:6e:72:e1:83:a8:34:2b:12:
27:2e:0f:d4:9f:89:d2:8f:24:76:53:62:ff:3a:32:0a:c6:4a:
ca:d1:dc:aa:39:93:dd:c6:66:c3:89:1d:46:69:2f:a8:7c:42:
d5:d7:29:81:c3:d7:07:43:bd:aa:4d:04:52:5e:9e:15:fc:6b:
97:e3:b0:4a:b6:3f:cf:c7:47:b8:41:8f:a1:81:c1:12:16:48:
87:41:3f:fb:88:a2:ce:11:18:54:11:b9:5a:d2:1f:2f:93:dc:
de:a9:d0:2f:7c:0d:be:1c:6f:3a:15:a3:16:d1:91:2e:c7:33:
83:ed:84:25:18:1d:6c:7f:9b:4d:6d:da:06:8d:30:a6:7d:f9:
8d:24:28:45:17:50:98:c9:e3:13:ae:44:f8:df:07:7b:f3:9a:
d1:e0:7a:80
-----BEGIN CERTIFICATE-----
MIIDUjCCAjqgAwIBAgIQauQpFUGUdWc8f4Um/zc9VTANBgkqhkiG9w0BAQsFADAY
MRYwFAYDVQQKEw1jbHVzdGVyLmxvY2FsMB4XDTIzMTAyMDA2NTU1NFoXDTIzMTAy
MTA2NTc1NFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOoMXiC
0/ukAS4Cw3CG+/Tj+EL8Ts6X0FpquRvr/SytFkyKEqAj/uo8fT8/a7VsxdP+9Qlo
uQPHHQjvluMaYLJenMb/E0IUCAz8ZiiYZdJJ75/Yw75wEhIk1iVdmetjThlRE8kJ
c3m4rAfuopl1Yrv2Mkja+2kyzM/xfu/o3Y+IssPLc7DtpmDiPOGyfvSdrRy+nbob
GuQMqKem6vX2lkd0d736Sl+9BZuL59VJJyIwW3niYqhxYDaKpnlBdgqv24/75PP6
GTy9SUg5ln8WJA7GDSABnHAyzR2SMuCzcrsaeKxJsv+QNk3Q0OhcAgV8x7momXIO
SIz1QX3OjZJzLDsCAwEAAaOBrzCBrDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU
nU6TYWVbR4vltB1spwaeNa2bigswTAYDVR0RAQH/BEIwQIY+c3BpZmZlOi8vY2x1
c3Rlci5sb2NhbC9ucy9kZWZhdWx0L3NhL210bHMtcmVzdHRlbXBsYXRlLWV4YW1w
bGUwDQYJKoZIhvcNAQELBQADggEBAG+t+NLhHkWZY54KbAYoFPt/9m3CTOYvQyg0
XqIWiA0c+4DTvXJ+zGNKkVQz6Q27vJJGE2gKn4MlnA2KzY9B1PPFtkg5BNqeelEA
PDQVxSDJANGsU046bGimbIodwOYTBkVWE9VucuGDqDQrEicuD9SfidKPJHZTYv86
MgrGSsrR3Ko5k93GZsOJHUZpL6h8QtXXKYHD1wdDvapNBFJenhX8a5fjsEq2P8/H
R7hBj6GBwRIWSIdBP/uIos4RGFQRuVrSHy+T3N6p0C98Db4cbzoVoxbRkS7HM4Pt
hCUYHWx/m01t2gaNMKZ9+Y0kKEUXUJjJ4xOuRPjfB3vzmtHgeoA=
-----END CERTIFICATE-----
```
#### 双向TLS
下面以RestTemplate客户端及SpringMVC(Tomcat)服务器为例,给出简单的使用示例。
将示例应用部署至K8s并通过如下命令进入RestTemplate应用
```shell
kubectl exec -c istio-proxy -it ${resttemplate_pod_name} -- bash
```
在容器内使用如下命令通过RestTemplate客户端向Tomcat服务器发送请求
```shell
curl -k https://localhost:${port}/resttemplate/getMvc
```
请求成功SpringMVC(Tomcat)服务器将会收到客户端所携带的证书。证书中包含应用的身份信息,即 ServiceAccount 。通过如下命令查看服务器所在pod的日志可以发现所收到的客户端证书
```shell
kubectl logs ${springmvc_pod_name}
```
某次请求中收到的客户端证书如下从证书中的SubjectAlternativeName字段可以识别出证书来自 ServiceAccount 为“mtls-resttemplate-example”的应用
```
[
[
Version: V3
Subject:
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
Key: Sun RSA public key, 2048 bits
params: null
modulus: 26719221528184068732015315264288604373679826539282478426097969053974382861354826542051876328932528175642713092529641146295102033253211316495853946817283249533389050199752927749843698539537721469963691881711082844075757454244576898594432695045339671702416660979527239503695271512833660067801224544638113738271334257178208382924689930111364189968004443796717959536203482853831898504813939931700174503652220712246767390007530274164485324125515933992826096083933791713331987686469400680968433332825899402171823122204857159285181103975487627104678111438273834092923367973802365694241887849829238593076455058952257637919803
public exponent: 65537
Validity: [From: Fri Oct 20 14:55:54 CST 2023,
To: Sat Oct 21 14:57:54 CST 2023]
Issuer: O=cluster.local
SerialNumber: [ 6ae42915 41947567 3c7f8526 ff373d55]
Certificate Extensions: 5
[1]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 9D 4E 93 61 65 5B 47 8B E5 B4 1D 6C A7 06 9E 35 .N.ae[G....l...5
0010: AD 9B 8A 0B ....
]
]
[2]: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
CA:false
PathLen: undefined
]
[3]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
serverAuth
clientAuth
]
[4]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
DigitalSignature
Key_Encipherment
]
[5]: ObjectId: 2.5.29.17 Criticality=true
SubjectAlternativeName [
URIName: spiffe://cluster.local/ns/default/sa/mtls-resttemplate-example
]
]
Algorithm: [SHA256withRSA]
Signature:
0000: 6F AD F8 D2 E1 1E 45 99 63 9E 0A 6C 06 28 14 FB o.....E.c..l.(..
0010: 7F F6 6D C2 4C E6 2F 43 28 34 5E A2 16 88 0D 1C ..m.L./C(4^.....
0020: FB 80 D3 BD 72 7E CC 63 4A 91 54 33 E9 0D BB BC ....r..cJ.T3....
0030: 92 46 13 68 0A 9F 83 25 9C 0D 8A CD 8F 41 D4 F3 .F.h...%.....A..
0040: C5 B6 48 39 04 DA 9E 7A 51 00 3C 34 15 C5 20 C9 ..H9...zQ.<4.. .
0050: 00 D1 AC 53 4E 3A 6C 68 A6 6C 8A 1D C0 E6 13 06 ...SN:lh.l......
0060: 45 56 13 D5 6E 72 E1 83 A8 34 2B 12 27 2E 0F D4 EV..nr...4+.'...
0070: 9F 89 D2 8F 24 76 53 62 FF 3A 32 0A C6 4A CA D1 ....$vSb.:2..J..
0080: DC AA 39 93 DD C6 66 C3 89 1D 46 69 2F A8 7C 42 ..9...f...Fi/..B
0090: D5 D7 29 81 C3 D7 07 43 BD AA 4D 04 52 5E 9E 15 ..)....C..M.R^..
00A0: FC 6B 97 E3 B0 4A B6 3F CF C7 47 B8 41 8F A1 81 .k...J.?..G.A...
00B0: C1 12 16 48 87 41 3F FB 88 A2 CE 11 18 54 11 B9 ...H.A?......T..
00C0: 5A D2 1F 2F 93 DC DE A9 D0 2F 7C 0D BE 1C 6F 3A Z../...../....o:
00D0: 15 A3 16 D1 91 2E C7 33 83 ED 84 25 18 1D 6C 7F .......3...%..l.
00E0: 9B 4D 6D DA 06 8D 30 A6 7D F9 8D 24 28 45 17 50 .Mm...0....$(E.P
00F0: 98 C9 E3 13 AE 44 F8 DF 07 7B F3 9A D1 E0 7A 80 .....D........z.
]
```
#### 流量模式切换
通过如下命令可以实现动态地将流量模式切换为http
```shell
curl -k 'https://localhost:${port}/actuator/sds' --header 'Content-Type: application/json' --data '{ "isTls":false }'
```
通过观察应用日志可以看到应用以http模式启动
![changeToHttp](pic/changeToHttp.png)
将流量模式切换为https
```shell
curl -k 'http://localhost:${port}/actuator/sds' --header 'Content-Type:
application/json' --data '{ "isTls":true }'
```
观察应用日志可以看到应用以https模式启动
![changeToHttps](pic/changeToHttps.png)
### AuthorizationPolicy principal策略
以Openfeign客户端请求Webflux(Netty)服务器为例为Webflux(Netty)应用所在pod上应用配置如下授权策略即允许服务账户为"cluster.local/ns/default/sa/mtls-openfeign-example"的服务访问:
```yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: default
spec:
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/mtls-openfeign-example"]
```
在容器内使用如下命令通过Openfeign客户端向Webflux(Netty)服务器发送请求:
```shell
curl -k https://localhost:${port}/openfeign/getWebflux
```
服务器响应:
```
webflux-server received request from client
```
更新授权策略如下,拒绝服务账户为"cluster.local/ns/default/sa/mtls-openfeign-example"的服务访问:
```yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: default
spec:
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/mtls-openfeign-example"]
```
再次向Webflux(Netty)服务器发送请求,请求失败,查看客户端应用日志,可以看到请求未授权。
![401Unauthorized](pic/401Unauthorized.png)

@ -0,0 +1,325 @@
# Mtls Example
## Project Instruction
This project demonstrates how to utilize Istio issued certificates to achieve mutual TLS capability between Spring Cloud Alibaba (hereinafter referred to as: SCA) applications. Currently for the server-side support for Spring MVC and Spring WebFlux application adaptation , and for feign, resttemplate and other client-side implementation , provides a hot update capability of the ssl context , the configuration can be automatically updated istio certificate.
## Preparation
### Install K8s
Please refer to [tools](https://kubernetes.io/docs/tasks/tools/) chapter of K8s document.
### Install Helm
Refer to the Helm [[Installation Guide]](https://helm.sh/docs/intro/install/).
### Enable Istio on K8s
Please refer to [install](https://istio.io/latest/docs/setup/install/) chapter of Istio document.
### Application Deployment
Deploy the sample project mtls-example in K8s with the following command:
```shell
helm install mtls-demo .
```
After successful deployment, view the pod information as follows:
![pods](pic/pods.png)
## Demo
### Connect to Istio
Before launching the example for demonstration, let's look at how a Spring Cloud application accesses Istio and provides authentication. This section is only for you to understand how to use it. The config has been filled in this example and you may not need to modify it.
1. Modify the `pom.xml` file to introduce the Istio rules adapter, the mtls module, the governance-auth module, and the actuator dependency:
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-governance-auth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```
2. Refer to the [documentation](https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-docs/src/main/asciidoc/mtls.adoc) for the implementation of the interface with the ` Istio` control plane and Service Account configuration.
### Quick Start
#### View Certificates
After deploying the sample application to K8s and configuring the Service Account of the application, in the pod where the application resides, you can view the certificates issued for the current application with the following command:
```shell
openssl s_client -connect 127.0.0.1:${port} </dev/null 2>/dev/null | openssl x509 -inform pem -text
```
Taking the SpringMVC (Tomcat) server as an example, you can see the spiffe encoded ServiceAccount content to be used as the Subject Alternative Name:
```
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
6a:e4:29:15:41:94:75:67:3c:7f:85:26:ff:37:3d:55
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = cluster.local
Validity
Not Before: Oct 20 06:55:54 2023 GMT
Not After : Oct 21 06:57:54 2023 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:d3:a8:31:78:82:d3:fb:a4:01:2e:02:c3:70:86:
fb:f4:e3:f8:42:fc:4e:ce:97:d0:5a:6a:b9:1b:eb:
fd:2c:ad:16:4c:8a:12:a0:23:fe:ea:3c:7d:3f:3f:
6b:b5:6c:c5:d3:fe:f5:09:68:b9:03:c7:1d:08:ef:
96:e3:1a:60:b2:5e:9c:c6:ff:13:42:14:08:0c:fc:
66:28:98:65:d2:49:ef:9f:d8:c3:be:70:12:12:24:
d6:25:5d:99:eb:63:4e:19:51:13:c9:09:73:79:b8:
ac:07:ee:a2:99:75:62:bb:f6:32:48:da:fb:69:32:
cc:cf:f1:7e:ef:e8:dd:8f:88:b2:c3:cb:73:b0:ed:
a6:60:e2:3c:e1:b2:7e:f4:9d:ad:1c:be:9d:ba:1b:
1a:e4:0c:a8:a7:a6:ea:f5:f6:96:47:74:77:bd:fa:
4a:5f:bd:05:9b:8b:e7:d5:49:27:22:30:5b:79:e2:
62:a8:71:60:36:8a:a6:79:41:76:0a:af:db:8f:fb:
e4:f3:fa:19:3c:bd:49:48:39:96:7f:16:24:0e:c6:
0d:20:01:9c:70:32:cd:1d:92:32:e0:b3:72:bb:1a:
78:ac:49:b2:ff:90:36:4d:d0:d0:e8:5c:02:05:7c:
c7:b9:a8:99:72:0e:48:8c:f5:41:7d:ce:8d:92:73:
2c:3b
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:9D:4E:93:61:65:5B:47:8B:E5:B4:1D:6C:A7:06:9E:35:AD:9B:8A:0B
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/default/sa/mtls-resttemplate-example
Signature Algorithm: sha256WithRSAEncryption
6f:ad:f8:d2:e1:1e:45:99:63:9e:0a:6c:06:28:14:fb:7f:f6:
6d:c2:4c:e6:2f:43:28:34:5e:a2:16:88:0d:1c:fb:80:d3:bd:
72:7e:cc:63:4a:91:54:33:e9:0d:bb:bc:92:46:13:68:0a:9f:
83:25:9c:0d:8a:cd:8f:41:d4:f3:c5:b6:48:39:04:da:9e:7a:
51:00:3c:34:15:c5:20:c9:00:d1:ac:53:4e:3a:6c:68:a6:6c:
8a:1d:c0:e6:13:06:45:56:13:d5:6e:72:e1:83:a8:34:2b:12:
27:2e:0f:d4:9f:89:d2:8f:24:76:53:62:ff:3a:32:0a:c6:4a:
ca:d1:dc:aa:39:93:dd:c6:66:c3:89:1d:46:69:2f:a8:7c:42:
d5:d7:29:81:c3:d7:07:43:bd:aa:4d:04:52:5e:9e:15:fc:6b:
97:e3:b0:4a:b6:3f:cf:c7:47:b8:41:8f:a1:81:c1:12:16:48:
87:41:3f:fb:88:a2:ce:11:18:54:11:b9:5a:d2:1f:2f:93:dc:
de:a9:d0:2f:7c:0d:be:1c:6f:3a:15:a3:16:d1:91:2e:c7:33:
83:ed:84:25:18:1d:6c:7f:9b:4d:6d:da:06:8d:30:a6:7d:f9:
8d:24:28:45:17:50:98:c9:e3:13:ae:44:f8:df:07:7b:f3:9a:
d1:e0:7a:80
-----BEGIN CERTIFICATE-----
MIIDUjCCAjqgAwIBAgIQauQpFUGUdWc8f4Um/zc9VTANBgkqhkiG9w0BAQsFADAY
MRYwFAYDVQQKEw1jbHVzdGVyLmxvY2FsMB4XDTIzMTAyMDA2NTU1NFoXDTIzMTAy
MTA2NTc1NFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOoMXiC
0/ukAS4Cw3CG+/Tj+EL8Ts6X0FpquRvr/SytFkyKEqAj/uo8fT8/a7VsxdP+9Qlo
uQPHHQjvluMaYLJenMb/E0IUCAz8ZiiYZdJJ75/Yw75wEhIk1iVdmetjThlRE8kJ
c3m4rAfuopl1Yrv2Mkja+2kyzM/xfu/o3Y+IssPLc7DtpmDiPOGyfvSdrRy+nbob
GuQMqKem6vX2lkd0d736Sl+9BZuL59VJJyIwW3niYqhxYDaKpnlBdgqv24/75PP6
GTy9SUg5ln8WJA7GDSABnHAyzR2SMuCzcrsaeKxJsv+QNk3Q0OhcAgV8x7momXIO
SIz1QX3OjZJzLDsCAwEAAaOBrzCBrDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU
nU6TYWVbR4vltB1spwaeNa2bigswTAYDVR0RAQH/BEIwQIY+c3BpZmZlOi8vY2x1
c3Rlci5sb2NhbC9ucy9kZWZhdWx0L3NhL210bHMtcmVzdHRlbXBsYXRlLWV4YW1w
bGUwDQYJKoZIhvcNAQELBQADggEBAG+t+NLhHkWZY54KbAYoFPt/9m3CTOYvQyg0
XqIWiA0c+4DTvXJ+zGNKkVQz6Q27vJJGE2gKn4MlnA2KzY9B1PPFtkg5BNqeelEA
PDQVxSDJANGsU046bGimbIodwOYTBkVWE9VucuGDqDQrEicuD9SfidKPJHZTYv86
MgrGSsrR3Ko5k93GZsOJHUZpL6h8QtXXKYHD1wdDvapNBFJenhX8a5fjsEq2P8/H
R7hBj6GBwRIWSIdBP/uIos4RGFQRuVrSHy+T3N6p0C98Db4cbzoVoxbRkS7HM4Pt
hCUYHWx/m01t2gaNMKZ9+Y0kKEUXUJjJ4xOuRPjfB3vzmtHgeoA=
-----END CERTIFICATE-----
```
#### Mutual TLS
The following RestTemplate client and SpringMVC (Tomcat) server as an example, to give a simple example of use.
Deploy the sample application to K8s and enter the RestTemplate application with the following command:
```shell
kubectl exec -c istio-proxy -it ${resttemplate_pod_name} -- bash
```
Use the following command within the container to send a request to the Tomcat server via the RestTemplate client:
```shell
curl -k https://localhost:${port}/resttemplate/getMvc
```
After a successful request, the SpringMVC (Tomcat) server will receive the certificate carried by the client. The certificate contains information about the identity of the application, i.e. ServiceAccount. The client certificate received can be found by viewing the logs of the pod where the server is located with the following command:
```shell
kubectl logs ${springmvc_pod_name}
```
The client certificate received in one of the requests is as follows. From the SubjectAlternativeName field in the certificate, you can identify that the certificate is from the application with ServiceAccount "mtls-resttemplate-example":
```
[
[
Version: V3
Subject:
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
Key: Sun RSA public key, 2048 bits
params: null
modulus: 26719221528184068732015315264288604373679826539282478426097969053974382861354826542051876328932528175642713092529641146295102033253211316495853946817283249533389050199752927749843698539537721469963691881711082844075757454244576898594432695045339671702416660979527239503695271512833660067801224544638113738271334257178208382924689930111364189968004443796717959536203482853831898504813939931700174503652220712246767390007530274164485324125515933992826096083933791713331987686469400680968433332825899402171823122204857159285181103975487627104678111438273834092923367973802365694241887849829238593076455058952257637919803
public exponent: 65537
Validity: [From: Fri Oct 20 14:55:54 CST 2023,
To: Sat Oct 21 14:57:54 CST 2023]
Issuer: O=cluster.local
SerialNumber: [ 6ae42915 41947567 3c7f8526 ff373d55]
Certificate Extensions: 5
[1]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 9D 4E 93 61 65 5B 47 8B E5 B4 1D 6C A7 06 9E 35 .N.ae[G....l...5
0010: AD 9B 8A 0B ....
]
]
[2]: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
CA:false
PathLen: undefined
]
[3]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
serverAuth
clientAuth
]
[4]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
DigitalSignature
Key_Encipherment
]
[5]: ObjectId: 2.5.29.17 Criticality=true
SubjectAlternativeName [
URIName: spiffe://cluster.local/ns/default/sa/mtls-resttemplate-example
]
]
Algorithm: [SHA256withRSA]
Signature:
0000: 6F AD F8 D2 E1 1E 45 99 63 9E 0A 6C 06 28 14 FB o.....E.c..l.(..
0010: 7F F6 6D C2 4C E6 2F 43 28 34 5E A2 16 88 0D 1C ..m.L./C(4^.....
0020: FB 80 D3 BD 72 7E CC 63 4A 91 54 33 E9 0D BB BC ....r..cJ.T3....
0030: 92 46 13 68 0A 9F 83 25 9C 0D 8A CD 8F 41 D4 F3 .F.h...%.....A..
0040: C5 B6 48 39 04 DA 9E 7A 51 00 3C 34 15 C5 20 C9 ..H9...zQ.<4.. .
0050: 00 D1 AC 53 4E 3A 6C 68 A6 6C 8A 1D C0 E6 13 06 ...SN:lh.l......
0060: 45 56 13 D5 6E 72 E1 83 A8 34 2B 12 27 2E 0F D4 EV..nr...4+.'...
0070: 9F 89 D2 8F 24 76 53 62 FF 3A 32 0A C6 4A CA D1 ....$vSb.:2..J..
0080: DC AA 39 93 DD C6 66 C3 89 1D 46 69 2F A8 7C 42 ..9...f...Fi/..B
0090: D5 D7 29 81 C3 D7 07 43 BD AA 4D 04 52 5E 9E 15 ..)....C..M.R^..
00A0: FC 6B 97 E3 B0 4A B6 3F CF C7 47 B8 41 8F A1 81 .k...J.?..G.A...
00B0: C1 12 16 48 87 41 3F FB 88 A2 CE 11 18 54 11 B9 ...H.A?......T..
00C0: 5A D2 1F 2F 93 DC DE A9 D0 2F 7C 0D BE 1C 6F 3A Z../...../....o:
00D0: 15 A3 16 D1 91 2E C7 33 83 ED 84 25 18 1D 6C 7F .......3...%..l.
00E0: 9B 4D 6D DA 06 8D 30 A6 7D F9 8D 24 28 45 17 50 .Mm...0....$(E.P
00F0: 98 C9 E3 13 AE 44 F8 DF 07 7B F3 9A D1 E0 7A 80 .....D........z.
]
```
#### Traffic Mode Switching
Dynamically switching the traffic mode to http can be achieved with the following command:
```shell
curl -k 'https://localhost:${port}/actuator/sds' --header 'Content-Type: application/json' --data '{ "isTls":false }'
```
Observing the application logs, you can see that the application was started in http mode:
![changeToHttp](pic/changeToHttp.png)
Switch the traffic pattern to https:
```shell
curl -k 'http://localhost:${port}/actuator/sds' --header 'Content-Type:
application/json' --data '{ "isTls":true }'
```
Observing the application logs, you can see that the application was started in https mode:
![changeToHttps](pic/changeToHttps.png)
### AuthorizationPolicy principal
Take the example of an Openfeign client requesting a Webflux (Netty) server, and configure the following authorization policy for the application on the pod where the Webflux (Netty) application resides, i.e., allow access to the service with the service account "cluster.local/ns/default/sa/mtls-openfeign-example":
```yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: default
spec:
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/mtls-openfeign-example"]
```
Use the following command within the container to send a request to the Webflux (Netty) server via the Openfeign client:
```shell
curl -k https://localhost:${port}/openfeign/getWebflux
```
Server Response:
```
webflux-server received request from client
```
Update the authorization policy as follows to deny access from services with service account "cluster.local/ns/default/sa/mtls-openfeign-example":
```yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: default
spec:
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/mtls-openfeign-example"]
```
Send the request to the Webflux (Netty) server again, the request fails, check the client application logs and you can see that the request is not authorized.
![401Unauthorized](pic/401Unauthorized.png)

@ -30,6 +30,7 @@
<module>spring-cloud-starter-opensergo-adapter</module>
<module>spring-cloud-starter-alibaba-governance-auth</module>
<module>spring-cloud-starter-alibaba-governance-routing</module>
<module>spring-cloud-starter-alibaba-governance-mtls</module>
<module>spring-cloud-alibaba-commons</module>
</modules>

@ -14,23 +14,16 @@
* limitations under the License.
*/
package com.alibaba.cloud.governance.istio;
import java.util.concurrent.ScheduledThreadPoolExecutor;
package com.alibaba.cloud.commons.governance;
/**
* @author musi
* @author <a href="liuziming@buaa.edu.cn"></a>
* @since 2.2.10-RC1
* @since 2.2.10-RC1 It means Spring Cloud Alibaba has got the config from control plane.
*/
public class XdsScheduledThreadPool extends ScheduledThreadPoolExecutor {
public XdsScheduledThreadPool(XdsConfigProperties xdsConfigProperties) {
this(xdsConfigProperties.getPollingPoolSize());
}
public class ControlPlaneInitedBean {
public XdsScheduledThreadPool(int corePoolSize) {
super(corePoolSize);
public ControlPlaneInitedBean() {
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.commons.governance.tls;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ServerTlsModeHolder {
private static final Logger log = LoggerFactory.getLogger(ServerTlsModeHolder.class);
private static volatile Boolean isTls;
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ServerTlsModeHolder() {
}
public static void init(boolean initValue) {
try {
rwLock.writeLock().lock();
if (isTls == null) {
isTls = initValue;
}
}
finally {
rwLock.writeLock().unlock();
}
}
public static void setTlsMode(boolean isTls) {
try {
rwLock.writeLock().lock();
ServerTlsModeHolder.isTls = isTls;
}
finally {
rwLock.writeLock().unlock();
}
}
public static Boolean getTlsMode() {
try {
rwLock.readLock().lock();
return isTls;
}
finally {
rwLock.readLock().unlock();
}
}
// Verify whether the tls mode is initialized and also updated.
public static boolean canModeUpdate(boolean isTls) {
try {
rwLock.readLock().lock();
return ServerTlsModeHolder.isTls != null
&& !Objects.equals(ServerTlsModeHolder.isTls, isTls);
}
finally {
rwLock.readLock().unlock();
}
}
}

@ -0,0 +1,45 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.governance.auth.util;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CertUtil {
private static final Logger log = LoggerFactory.getLogger(CertUtil.class);
private CertUtil() {
}
public static String getIstioIdentity(X509Certificate x509Certificate) {
try {
Collection<List<?>> san = x509Certificate.getSubjectAlternativeNames();
return (String) san.iterator().next().get(1);
}
catch (Exception e) {
log.error("Failed to get istio SAN from X509Certificate", e);
}
return "";
}
}

@ -22,6 +22,7 @@ import java.util.Map;
import com.alibaba.cloud.commons.governance.auth.condition.AuthCondition;
import com.alibaba.cloud.commons.governance.auth.rule.AuthRule;
import com.alibaba.cloud.commons.governance.auth.rule.JwtRule;
import com.alibaba.cloud.commons.governance.tls.ServerTlsModeHolder;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.commons.matcher.Matcher;
import com.alibaba.cloud.governance.auth.repository.AuthRepository;
@ -34,10 +35,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
/**
* Use a abstract rule tree to validate the request. First, if the rules are all empty, we
* just return true. Secondly, if any deny rule matches the request, we just return false.
* Thirdly, if the allow rules are empty, we just return true. Last, if any allow rule
* matches the request, we just return true, or we return false.
* Use an abstract rule tree to validate the request. First, if the rules are all empty,
* we just return true. Secondly, if any deny rule matches the request, we just return
* false. Thirdly, if the allow rules are empty, we just return true. Last, if any allow
* rule matches the request, we just return true, or we return false.
*
* @author musi
* @author <a href="liuziming@buaa.edu.cn"></a>
@ -49,8 +50,11 @@ public class AuthValidator {
private final AuthRepository authRepository;
private final boolean isTls;
public AuthValidator(AuthRepository authRepository) {
this.authRepository = authRepository;
this.isTls = ServerTlsModeHolder.getTlsMode() != null;
}
public boolean validate(UnifiedHttpRequest request) {
@ -143,6 +147,11 @@ public class AuthValidator {
return matcher.match(request.getMethod());
case PATHS:
return matcher.match(request.getPath());
case IDENTITY:
if (!isTls) {
return true;
}
return matcher.match(request.getPrincipal());
case REQUEST_PRINCIPALS:
case AUTH_AUDIENCES:
case AUTH_PRESENTERS:
@ -236,9 +245,11 @@ public class AuthValidator {
private JwtClaims jwtClaims;
private String principal;
private UnifiedHttpRequest(String sourceIp, String destIp, String remoteIp,
String host, int port, String method, String path, HttpHeaders headers,
MultiValueMap<String, String> params) {
MultiValueMap<String, String> params, String principal) {
this.sourceIp = sourceIp;
this.destIp = destIp;
this.remoteIp = remoteIp;
@ -248,6 +259,7 @@ public class AuthValidator {
this.path = path;
this.headers = headers;
this.params = params;
this.principal = principal;
}
public String getSourceIp() {
@ -290,6 +302,10 @@ public class AuthValidator {
return jwtClaims;
}
public String getPrincipal() {
return principal;
}
public static class UnifiedHttpRequestBuilder {
private String sourceIp;
@ -308,6 +324,8 @@ public class AuthValidator {
private HttpHeaders headers;
private String principal;
private MultiValueMap<String, String> params;
public UnifiedHttpRequestBuilder setSourceIp(String sourceIp) {
@ -356,9 +374,18 @@ public class AuthValidator {
return this;
}
public String getPrincipal() {
return principal;
}
public UnifiedHttpRequestBuilder setPrincipal(String principal) {
this.principal = principal;
return this;
}
public UnifiedHttpRequest build() {
return new UnifiedHttpRequest(sourceIp, destIp, remoteIp, host, port,
method, path, headers, params);
method, path, headers, params, principal);
}
}

@ -17,7 +17,9 @@
package com.alibaba.cloud.governance.auth.webflux;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import com.alibaba.cloud.governance.auth.util.CertUtil;
import com.alibaba.cloud.governance.auth.util.IpUtil;
import com.alibaba.cloud.governance.auth.validator.AuthValidator;
import reactor.core.publisher.Mono;
@ -64,11 +66,19 @@ public class AuthWebFluxFilter implements WebFilter {
String path = request.getPath().value();
HttpHeaders headers = request.getHeaders();
MultiValueMap<String, String> params = request.getQueryParams();
String principal = "";
if (exchange.getRequest().getSslInfo() != null) {
X509Certificate[] certs = exchange.getRequest().getSslInfo()
.getPeerCertificates();
if (certs != null && certs.length > 0) {
principal = CertUtil.getIstioIdentity(certs[0]);
}
}
AuthValidator.UnifiedHttpRequest.UnifiedHttpRequestBuilder builder = new AuthValidator.UnifiedHttpRequest.UnifiedHttpRequestBuilder();
AuthValidator.UnifiedHttpRequest unifiedHttpRequest = builder.setDestIp(destIp)
.setRemoteIp(remoteIp).setSourceIp(sourceIp).setHost(host).setPort(port)
.setMethod(method).setPath(path).setHeaders(headers).setParams(params)
.build();
.setPrincipal(principal).build();
if (!authValidator.validate(unifiedHttpRequest)) {
return ret401(exchange);

@ -17,12 +17,14 @@
package com.alibaba.cloud.governance.auth.webmvc;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.cloud.governance.auth.util.CertUtil;
import com.alibaba.cloud.governance.auth.util.IpUtil;
import com.alibaba.cloud.governance.auth.validator.AuthValidator;
import org.slf4j.Logger;
@ -60,11 +62,17 @@ public class AuthWebInterceptor implements HandlerInterceptor {
int port = request.getLocalPort();
HttpHeaders headers = getHeaders(request);
MultiValueMap<String, String> params = getQueryParams(request);
String principal = "";
X509Certificate[] certs = (X509Certificate[]) request
.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
principal = CertUtil.getIstioIdentity(certs[0]);
}
AuthValidator.UnifiedHttpRequest.UnifiedHttpRequestBuilder builder = new AuthValidator.UnifiedHttpRequest.UnifiedHttpRequestBuilder();
AuthValidator.UnifiedHttpRequest unifiedHttpRequest = builder.setDestIp(destIp)
.setRemoteIp(remoteIp).setSourceIp(sourceIp).setHost(host).setPort(port)
.setMethod(method).setPath(path).setHeaders(headers).setParams(params)
.build();
.setPrincipal(principal).build();
if (!authValidator.validate(unifiedHttpRequest)) {
return ret401(response);
}

@ -19,6 +19,7 @@ package com.alibaba.cloud.governance.auth;
import java.io.File;
import java.nio.charset.Charset;
import com.alibaba.cloud.commons.governance.ControlPlaneInitedBean;
import com.alibaba.cloud.commons.io.FileUtils;
import com.alibaba.cloud.governance.auth.repository.AuthRepository;
import com.alibaba.cloud.governance.auth.validator.AuthValidator;
@ -31,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
@ -94,6 +96,11 @@ public class AuthenticationTest {
@ImportAutoConfiguration({ AuthValidatorAutoConfiguration.class })
public static class TestConfig {
@Bean
public ControlPlaneInitedBean dummyControlPlaneInitedBean() {
return new ControlPlaneInitedBean();
}
}
}

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-starters</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>spring-cloud-starter-alibaba-governance-mtls</artifactId>
<name>Spring Cloud Starter Alibaba Governance MTLS</name>
<properties>
<okhttp-version>3.10.0</okhttp-version>
<kickstart-version>8.1.5</kickstart-version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-xds-adapter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>${kickstart-version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -0,0 +1,123 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import java.util.List;
import com.alibaba.cloud.commons.governance.ControlPlaneInitedBean;
import com.alibaba.cloud.commons.governance.tls.ServerTlsModeHolder;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertUpdateCallback;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import com.alibaba.cloud.mtls.server.ApplicationRestarter;
import com.alibaba.cloud.mtls.server.netty.MtlsNettyServerCustomizer;
import com.alibaba.cloud.mtls.server.tomcat.MtlsTomcatConnectCustomizer;
import com.alibaba.cloud.nacos.registry.NacosRegistration;
import com.alibaba.cloud.nacos.registry.NacosRegistrationCustomizer;
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.netty.http.server.HttpServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.cloud.mtls.config.enabled", matchIfMissing = true)
@EnableConfigurationProperties(MtlsConfigProperties.class)
public class MtlsAutoConfiguration {
private static final Logger log = LoggerFactory
.getLogger(MtlsAutoConfiguration.class);
@Autowired
private MtlsConfigProperties mtlsConfigProperties;
@Bean
public MtlsSslStoreProvider mtlsSslStoreProvider(AbstractCertManager certManager) {
return new MtlsSslStoreProvider(certManager);
}
@Bean
public ApplicationRestarter applicationRestarter() {
return new ApplicationRestarter();
}
@Bean
public MtlsCertCallbackIniter mtlsClientCallbackIniter(
AbstractCertManager certManager,
@Autowired(required = false) List<CertUpdateCallback> callbacks) {
return new MtlsCertCallbackIniter(certManager, callbacks);
}
@Bean
public MtlsClientSSLContext mtlsSSLContext(MtlsSslStoreProvider mtlsSslStoreProvider,
AbstractCertManager abstractCertManager) {
return new MtlsClientSSLContext(mtlsSslStoreProvider, abstractCertManager);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class })
static class TomcatConnectionCustomizerConfiguration {
@Bean
public TomcatConnectorCustomizer mtlsCustomizer(
MtlsSslStoreProvider sslStoreProvider, AbstractCertManager certManager,
ControlPlaneInitedBean controlPlaneInitedBean) {
return new MtlsTomcatConnectCustomizer(sslStoreProvider, certManager);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ NacosRegistrationCustomizer.class, NacosRegistration.class })
static class NacosCustomizerConfiguration {
@Bean
public NacosRegistrationCustomizer nacosTlsCustomizer() {
return registration -> {
if (!ServerTlsModeHolder.getTlsMode()) {
log.warn("Fetch tls mode failed, use plaintext to transport");
return;
}
registration.getNacosDiscoveryProperties()
.setSecure(ServerTlsModeHolder.getTlsMode());
registration.getNacosDiscoveryProperties().getMetadata().put("secure",
"true");
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpServer.class)
public class NettyCustomizerConfiguration {
@Bean
public MtlsNettyServerCustomizer mtlsNettyCustomizer(
AbstractCertManager certManager, MtlsSslStoreProvider sslStoreProvider) {
return new MtlsNettyServerCustomizer(certManager, sslStoreProvider);
}
}
}

@ -0,0 +1,36 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import java.util.List;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertUpdateCallback;
class MtlsCertCallbackIniter {
MtlsCertCallbackIniter(AbstractCertManager certManager,
List<CertUpdateCallback> callbacks) {
if (callbacks == null || callbacks.isEmpty()) {
return;
}
for (CertUpdateCallback callback : callbacks) {
certManager.registerCallback(callback);
}
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import javax.annotation.PostConstruct;
import com.alibaba.cloud.commons.governance.tls.ServerTlsModeHolder;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author musi
* @author <a href="liuziming@buaa.edu.cn"></a>
* @since 2.2.10-RC1
*/
@ConfigurationProperties(MtlsConfigProperties.PREFIX)
public class MtlsConfigProperties {
private Boolean serverTls;
/**
* Prefix for mtls config.
*/
public static final String PREFIX = "spring.cloud.mtls.config";
public void setServerTls(boolean serverTls) {
this.serverTls = serverTls;
}
public boolean isServerTls() {
return serverTls;
}
@PostConstruct
public void postConstruct() {
if (serverTls == null) {
serverTls = true;
}
ServerTlsModeHolder.init(serverTls);
}
}

@ -0,0 +1,80 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import java.security.KeyStore;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertPair;
import com.alibaba.cloud.mtls.constants.MtlsConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.server.SslStoreProvider;
public class MtlsSslStoreProvider implements SslStoreProvider {
private static final Logger log = LoggerFactory.getLogger(MtlsSslStoreProvider.class);
private final AbstractCertManager certManager;
public MtlsSslStoreProvider(AbstractCertManager certManager) {
this.certManager = certManager;
}
public KeyStore getKeyStore(CertPair certPair) {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setKeyEntry(MtlsConstants.MTLS_DEFAULT_KEY_STORE_ALIAS,
certPair.getPrivateKey(), "".toCharArray(),
certPair.getCertificateChain());
return keyStore;
}
catch (Exception e) {
log.error("Unable to get key store", e);
}
return null;
}
@Override
public KeyStore getKeyStore() {
CertPair certPair = certManager.getCertPair();
return getKeyStore(certPair);
}
public KeyStore getTrustStore(CertPair certPair) {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry(MtlsConstants.MTLS_DEFAULT_TRUST_STORE_ALIAS,
certPair.getRootCA());
return keyStore;
}
catch (Exception e) {
log.error("Unable to get trust store", e);
}
return null;
}
@Override
public KeyStore getTrustStore() {
CertPair certPair = certManager.getCertPair();
return getTrustStore(certPair);
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.client;
import java.security.KeyStore;
import com.alibaba.cloud.governance.istio.sds.CertPair;
import com.alibaba.cloud.governance.istio.sds.CertUpdateCallback;
import com.alibaba.cloud.mtls.MtlsSslStoreProvider;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.SSLFactoryUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientCertUpdateCallback implements CertUpdateCallback {
private static final Logger log = LoggerFactory
.getLogger(ClientCertUpdateCallback.class);
private MtlsSslStoreProvider mtlsSslStoreProvider;
private SSLFactory originFactory;
public ClientCertUpdateCallback(MtlsSslStoreProvider mtlsSslStoreProvider,
SSLFactory originFactory) {
this.mtlsSslStoreProvider = mtlsSslStoreProvider;
this.originFactory = originFactory;
}
@Override
public synchronized void onUpdateCert(CertPair certPair) {
try {
KeyStore keyStore = mtlsSslStoreProvider.getKeyStore();
KeyStore trustStore = mtlsSslStoreProvider.getTrustStore();
SSLFactory factory = SSLFactory.builder().withUnsafeHostnameVerifier()
.withTrustMaterial(trustStore)
.withIdentityMaterial(keyStore, "".toCharArray()).build();
SSLFactoryUtils.reload(originFactory, factory);
}
catch (Throwable t) {
log.error("Failed to refresh x509KeyManager", t);
}
}
}

@ -0,0 +1,118 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.client;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.mtls.MtlsSslStoreProvider;
import nl.altindag.ssl.SSLFactory;
public class MtlsClientSSLContext {
private SSLFactory sslFactory;
public MtlsClientSSLContext(MtlsSslStoreProvider mtlsSslStoreProvider,
AbstractCertManager abstractCertManager) {
if (sslFactory == null) {
synchronized (MtlsClientSSLContext.class) {
if (sslFactory == null) {
KeyStore keyStore = mtlsSslStoreProvider.getKeyStore();
// init trust store
KeyStore trustStore = mtlsSslStoreProvider.getTrustStore();
sslFactory = SSLFactory.builder().withUnsafeHostnameVerifier()
.withSwappableIdentityMaterial().withSwappableTrustMaterial()
.withIdentityMaterial(keyStore, "".toCharArray())
.withTrustMaterial(trustStore).build();
abstractCertManager.registerCallback(new ClientCertUpdateCallback(
mtlsSslStoreProvider, sslFactory));
}
}
}
}
public SSLContext getSslContext() {
return sslFactory.getSslContext();
}
public SSLSocketFactory getSslSocketFactory() {
return sslFactory.getSslSocketFactory();
}
public SSLServerSocketFactory getSslServerSocketFactory() {
return sslFactory.getSslServerSocketFactory();
}
public Optional<X509ExtendedKeyManager> getKeyManager() {
return sslFactory.getKeyManager();
}
public Optional<KeyManagerFactory> getKeyManagerFactory() {
return sslFactory.getKeyManagerFactory();
}
public Optional<X509ExtendedTrustManager> getTrustManager() {
return sslFactory.getTrustManager();
}
public Optional<TrustManagerFactory> getTrustManagerFactory() {
return sslFactory.getTrustManagerFactory();
}
public List<X509Certificate> getTrustedCertificates() {
return sslFactory.getTrustedCertificates();
}
public HostnameVerifier getHostnameVerifier() {
return sslFactory.getHostnameVerifier();
}
public List<String> getCiphers() {
return sslFactory.getCiphers();
}
public List<String> getProtocols() {
return sslFactory.getProtocols();
}
public SSLParameters getSslParameters() {
return sslFactory.getSslParameters();
}
public SSLEngine getSSLEngine() {
return sslFactory.getSSLEngine();
}
public SSLEngine getSSLEngine(String peerHost, Integer peerPort) {
return sslFactory.getSSLEngine(peerHost, peerPort);
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.constants;
public final class MtlsConstants {
/**
* Mtls default key store alias key.
*/
public static final String MTLS_DEFAULT_KEY_STORE_ALIAS = "mtls-default-key-store";
/**
* Mtls default trust store alias key.
*/
public static final String MTLS_DEFAULT_TRUST_STORE_ALIAS = "mtls-default-trust-store";
private MtlsConstants() {
}
}

@ -0,0 +1,45 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.endpoint;
import com.alibaba.cloud.commons.governance.tls.ServerTlsModeHolder;
import com.alibaba.cloud.mtls.server.ApplicationRestarter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
/**
* @author musi
*/
@Endpoint(id = "sds")
public class MtlsEndpoint {
@Autowired
private ApplicationRestarter restarter;
@WriteOperation
public String updateTlsMode(boolean isTls) {
if (ServerTlsModeHolder.canModeUpdate(isTls)) {
ServerTlsModeHolder.setTlsMode(isTls);
restarter.restart();
}
return "update tls mode to " + isTls;
}
}

@ -0,0 +1,42 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.endpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
/**
* @author musi
*/
@ConditionalOnWebApplication
@ConditionalOnClass(Endpoint.class)
@ConditionalOnProperty(name = "spring.cloud.mtls.config.enabled", matchIfMissing = true)
public class MtlsEndpointAutoConfiguration {
@ConditionalOnMissingBean
@ConditionalOnAvailableEndpoint
@Bean
public MtlsEndpoint sdsEndpoint() {
return new MtlsEndpoint();
}
}

@ -0,0 +1,29 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.exception;
public class MtlsInitException extends RuntimeException {
public MtlsInitException(String message) {
super(message);
}
public MtlsInitException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,105 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server;
import java.io.Closeable;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ClassUtils;
public class ApplicationRestarter {
private ConfigurableApplicationContext context;
private SpringApplication application;
private String[] args;
private static final Log log = LogFactory.getLog(PreparedEventRecorder.class);
public void restart() {
Thread thread = new Thread(this::safeRestart);
thread.setDaemon(false);
thread.start();
}
public ApplicationRestarter() {
this.application = PreparedEventRecorder.getApplication();
this.context = PreparedEventRecorder.getContext();
this.args = PreparedEventRecorder.getArgs();
}
private Boolean safeRestart() {
try {
doRestart();
return true;
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.info("Could not doRestart", e);
}
else {
log.info("Could not doRestart: " + e.getMessage());
}
return false;
}
}
private synchronized void doRestart() {
if (this.context != null) {
this.application.setEnvironment(this.context.getEnvironment());
close();
// If running in a webapp then the context classloader is probably going to
// die so we need to revert to a safe place before starting again
overrideClassLoaderForRestart();
this.context = this.application.run(this.args);
}
}
private void close() {
ApplicationContext context = this.context;
while (context instanceof Closeable) {
try {
((Closeable) context).close();
}
catch (IOException e) {
log.error("Cannot close context: " + context.getId(), e);
}
context = context.getParent();
}
}
// @ManagedAttribute
public boolean isRunning() {
if (this.context != null) {
return this.context.isRunning();
}
return false;
}
private void overrideClassLoaderForRestart() {
ClassUtils.overrideThreadContextClassLoader(
this.application.getClass().getClassLoader());
}
}

@ -0,0 +1,87 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class PreparedEventRecorder
implements ApplicationListener<ApplicationPreparedEvent> {
private static final Log log = LogFactory.getLog(PreparedEventRecorder.class);
private static ConfigurableApplicationContext context;
private static SpringApplication application;
private static String[] args;
@Override
public void onApplicationEvent(ApplicationPreparedEvent input) {
synchronized (PreparedEventRecorder.class) {
context = input.getApplicationContext();
args = input.getArgs();
application = input.getSpringApplication();
application.addInitializers(new PostProcessorInitializer());
}
}
static ConfigurableApplicationContext getContext() {
return context;
}
static SpringApplication getApplication() {
return application;
}
static String[] getArgs() {
return args;
}
class PostProcessorInitializer
implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext context) {
context.registerBean(PostProcessor.class, () -> new PostProcessor());
}
}
class PostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof PreparedEventRecorder) {
return PreparedEventRecorder.this;
}
return bean;
}
}
}

@ -0,0 +1,296 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server.netty;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertPair;
import com.alibaba.cloud.governance.istio.sds.CertUpdateCallback;
import com.alibaba.cloud.mtls.MtlsSslStoreProvider;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.SslConfigurationValidator;
import org.springframework.boot.web.server.WebServerException;
public class MtlsNettyServerCustomizer implements NettyServerCustomizer {
private static final Logger log = LoggerFactory
.getLogger(MtlsNettyServerCustomizer.class);
private final AbstractCertManager certManager;
private final MtlsSslStoreProvider sslStoreProvider;
private SslContextDelegate context;
public MtlsNettyServerCustomizer(AbstractCertManager certManager,
MtlsSslStoreProvider sslStoreProvider) {
this.certManager = certManager;
this.sslStoreProvider = sslStoreProvider;
}
@Override
public HttpServer apply(HttpServer httpServer) {
// update certificate
certManager.registerCallback(new CertUpdateCallback() {
@Override
public synchronized void onUpdateCert(CertPair certPair) {
if (context != null) {
try {
context.setContext(getContextBuilder().build());
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
});
try {
if (context == null) {
context = new SslContextDelegate(getContextBuilder().build());
}
return httpServer
.secure(sslContextSpec -> sslContextSpec.sslContext(context));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private SslContextBuilder getContextBuilder() {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.WANT); // 请求客户端认证但不强制
SslContextBuilder builder = SslContextBuilder
.forServer(getKeyManagerFactory(ssl, this.sslStoreProvider))
.trustManager(getTrustManagerFactory(this.sslStoreProvider));
if (ssl.getEnabledProtocols() != null) {
builder.protocols(ssl.getEnabledProtocols());
}
if (ssl.getCiphers() != null) {
builder.ciphers(Arrays.asList(ssl.getCiphers()));
}
builder.clientAuth(ClientAuth.OPTIONAL); // 客户端认证模式为OPTIONAL。这意味着服务器会请求客户端证书但即使客户端不提供证书SSL/TLS握手仍将成功。
return builder;
}
private KeyManagerFactory getKeyManagerFactory(Ssl ssl,
MtlsSslStoreProvider sslStoreProvider) {
try {
KeyStore keyStore = null;
try {
if (sslStoreProvider != null) {
keyStore = sslStoreProvider.getKeyStore();
}
}
catch (Exception ex) {
throw new WebServerException("Could not load store: " + ex.getMessage(),
ex);
}
SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias());
KeyManagerFactory keyManagerFactory = null;
if (ssl.getKeyAlias() == null) {
keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
}
else {
keyManagerFactory = new ConfigurableAliasKeyManagerFactory(
ssl.getKeyAlias(), KeyManagerFactory.getDefaultAlgorithm());
}
/*
* char[] keyPassword = (ssl.getKeyPassword() != null) ?
* ssl.getKeyPassword().toCharArray() : null; if (keyPassword == null &&
* ssl.getKeyStorePassword() != null) { keyPassword =
* ssl.getKeyStorePassword().toCharArray(); }
*/
String keyPass = "";
keyManagerFactory.init(keyStore, keyPass.toCharArray());
return keyManagerFactory;
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private TrustManagerFactory getTrustManagerFactory(
MtlsSslStoreProvider sslStoreProvider) {
try {
KeyStore store = null;
try {
if (sslStoreProvider != null) {
store = sslStoreProvider.getTrustStore();
}
}
catch (Exception ex) {
throw new WebServerException("Could not load store: " + ex.getMessage(),
ex);
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(store);
return trustManagerFactory;
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private static final class ConfigurableAliasKeyManagerFactory
extends KeyManagerFactory {
private ConfigurableAliasKeyManagerFactory(String alias, String algorithm)
throws NoSuchAlgorithmException {
this(KeyManagerFactory.getInstance(algorithm), alias, algorithm);
}
private ConfigurableAliasKeyManagerFactory(KeyManagerFactory delegate,
String alias, String algorithm) {
super(new ConfigurableAliasKeyManagerFactorySpi(delegate, alias),
delegate.getProvider(), algorithm);
}
}
private static final class ConfigurableAliasKeyManagerFactorySpi
extends KeyManagerFactorySpi {
private final KeyManagerFactory delegate;
private final String alias;
private ConfigurableAliasKeyManagerFactorySpi(KeyManagerFactory delegate,
String alias) {
this.delegate = delegate;
this.alias = alias;
}
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException,
UnrecoverableKeyException {
this.delegate.init(keyStore, chars);
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
throw new InvalidAlgorithmParameterException(
"Unsupported ManagerFactoryParameters");
}
@Override
protected KeyManager[] engineGetKeyManagers() {
return Arrays.stream(this.delegate.getKeyManagers())
.filter(X509ExtendedKeyManager.class::isInstance)
.map(X509ExtendedKeyManager.class::cast).map(this::wrap)
.toArray(KeyManager[]::new);
}
private ConfigurableAliasKeyManager wrap(X509ExtendedKeyManager keyManager) {
return new ConfigurableAliasKeyManager(keyManager, this.alias);
}
}
private static final class ConfigurableAliasKeyManager
extends X509ExtendedKeyManager {
private final X509ExtendedKeyManager delegate;
private final String alias;
private ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager,
String alias) {
this.delegate = keyManager;
this.alias = alias;
}
@Override
public String chooseEngineClientAlias(String[] strings, Principal[] principals,
SSLEngine sslEngine) {
return this.delegate.chooseEngineClientAlias(strings, principals, sslEngine);
}
@Override
public String chooseEngineServerAlias(String s, Principal[] principals,
SSLEngine sslEngine) {
return (this.alias != null) ? this.alias
: this.delegate.chooseEngineServerAlias(s, principals, sslEngine);
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers,
Socket socket) {
return this.delegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) {
return this.delegate.chooseServerAlias(keyType, issuers, socket);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return this.delegate.getCertificateChain(alias);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
}
}

@ -0,0 +1,70 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server.netty;
import java.util.List;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSessionContext;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.ApplicationProtocolNegotiator;
import io.netty.handler.ssl.SslContext;
public class SslContextDelegate extends SslContext {
private SslContext context;
public SslContextDelegate(SslContext context) {
this.context = context;
}
public void setContext(SslContext context) {
this.context = context;
}
@Override
public boolean isClient() {
return context.isClient();
}
@Override
public List<String> cipherSuites() {
return context.cipherSuites();
}
@Override
public ApplicationProtocolNegotiator applicationProtocolNegotiator() {
return context.applicationProtocolNegotiator();
}
@Override
public SSLEngine newEngine(ByteBufAllocator byteBufAllocator) {
return context.newEngine(byteBufAllocator);
}
@Override
public SSLEngine newEngine(ByteBufAllocator byteBufAllocator, String s, int i) {
return context.newEngine(byteBufAllocator, s, i);
}
@Override
public SSLSessionContext sessionContext() {
return context.sessionContext();
}
}

@ -0,0 +1,160 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server.tomcat;
import com.alibaba.cloud.commons.governance.tls.ServerTlsModeHolder;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertPair;
import com.alibaba.cloud.governance.istio.sds.CertUpdateCallback;
import com.alibaba.cloud.mtls.MtlsSslStoreProvider;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11JsseProtocol;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.StringUtils;
public class MtlsTomcatConnectCustomizer
implements TomcatConnectorCustomizer, ApplicationContextAware {
private static final Logger log = LoggerFactory
.getLogger(MtlsTomcatConnectCustomizer.class);
private final AbstractCertManager certManager;
private final MtlsSslStoreProvider sslStoreProvider;
private ApplicationContext applicationContext;
public MtlsTomcatConnectCustomizer(MtlsSslStoreProvider sslStoreProvider,
AbstractCertManager certManager) {
this.certManager = certManager;
this.sslStoreProvider = sslStoreProvider;
}
@Override
public void customize(Connector connector) {
if (!validateContext()) {
return;
}
// When the certificate is expired, we refresh the server certificate.
certManager.registerCallback(new CertUpdateCallback() {
@Override
public synchronized void onUpdateCert(CertPair certPair) {
try {
if (!validateContext()) {
return;
}
ProtocolHandler protocolHandler = connector.getProtocolHandler();
AbstractProtocol<?> abstractProtocol = (AbstractProtocol<?>) protocolHandler;
if (abstractProtocol instanceof AbstractHttp11Protocol<?>) {
AbstractHttp11Protocol<?> proto = ((AbstractHttp11Protocol<?>) abstractProtocol);
proto.reloadSslHostConfigs();
}
}
catch (Exception e) {
log.error("Failed to reload certificate of tomcat", e);
}
}
});
if (!ServerTlsModeHolder.getTlsMode()) {
log.warn("Fetch tls mode failed, use plaintext to transport");
return;
}
if (!ServerTlsModeHolder.getTlsMode()) {
return;
}
try {
ProtocolHandler handler = connector.getProtocolHandler();
AbstractHttp11JsseProtocol<?> protocol = (AbstractHttp11JsseProtocol<?>) handler;
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.WANT);
protocol.setSSLEnabled(true);
protocol.setSslProtocol(ssl.getProtocol());
configureSslClientAuth(protocol, ssl);
protocol.setKeyAlias(ssl.getKeyAlias());
String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers());
if (StringUtils.hasText(ciphers)) {
protocol.setCiphers(ciphers);
}
TomcatURLStreamHandlerFactory tomcatURLStreamHandlerFactory = TomcatURLStreamHandlerFactory
.getInstance();
SslStoreProviderUrlStreamHandlerFactory sslStoreProviderUrlStreamHandlerFactory = new SslStoreProviderUrlStreamHandlerFactory(
sslStoreProvider);
TomcatURLStreamHandlerFactory.release(
sslStoreProviderUrlStreamHandlerFactory.getClass().getClassLoader());
tomcatURLStreamHandlerFactory
.addUserFactory(sslStoreProviderUrlStreamHandlerFactory);
try {
if (sslStoreProvider.getKeyStore() != null) {
protocol.setKeystorePass("");
protocol.setKeystoreFile(
SslStoreProviderUrlStreamHandlerFactory.KEY_STORE_URL);
}
if (sslStoreProvider.getTrustStore() != null) {
protocol.setTruststorePass("");
protocol.setTruststoreFile(
SslStoreProviderUrlStreamHandlerFactory.TRUST_STORE_URL);
}
}
catch (Exception ex) {
throw new WebServerException("Could not load store: " + ex.getMessage(),
ex);
}
connector.setScheme("https");
connector.setSecure(true);
}
catch (Exception e) {
log.error("Custom tomcat ssl failed!", e);
}
}
private boolean validateContext() {
if (applicationContext instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
return context.isActive();
}
return true;
}
private void configureSslClientAuth(AbstractHttp11JsseProtocol<?> protocol, Ssl ssl) {
if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
protocol.setClientAuth(Boolean.TRUE.toString());
}
else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
protocol.setClientAuth("want");
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}

@ -0,0 +1,109 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls.server.tomcat;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.KeyStore;
import org.springframework.boot.web.server.SslStoreProvider;
public class SslStoreProviderUrlStreamHandlerFactory implements URLStreamHandlerFactory {
private static final String PROTOCOL = "springbootssl";
private static final String KEY_STORE_PATH = "keyStore";
static final String KEY_STORE_URL = PROTOCOL + ":" + KEY_STORE_PATH;
private static final String TRUST_STORE_PATH = "trustStore";
static final String TRUST_STORE_URL = PROTOCOL + ":" + TRUST_STORE_PATH;
// Must be a static variable, or we can not reload sslStoreProvider when we restart
// the spring context.
private static SslStoreProvider sslStoreProvider;
SslStoreProviderUrlStreamHandlerFactory(SslStoreProvider sslStoreProvider) {
SslStoreProviderUrlStreamHandlerFactory.sslStoreProvider = sslStoreProvider;
}
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
if (PROTOCOL.equals(protocol)) {
return new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL url) throws IOException {
try {
if (KEY_STORE_PATH.equals(url.getPath())) {
return new KeyStoreUrlConnection(url,
SslStoreProviderUrlStreamHandlerFactory.sslStoreProvider
.getKeyStore());
}
if (TRUST_STORE_PATH.equals(url.getPath())) {
return new KeyStoreUrlConnection(url,
SslStoreProviderUrlStreamHandlerFactory.sslStoreProvider
.getTrustStore());
}
}
catch (Exception ex) {
throw new IOException(ex);
}
throw new IOException("Invalid path: " + url.getPath());
}
};
}
return null;
}
private static final class KeyStoreUrlConnection extends URLConnection {
private final KeyStore keyStore;
private KeyStoreUrlConnection(URL url, KeyStore keyStore) {
super(url);
this.keyStore = keyStore;
}
@Override
public void connect() throws IOException {
}
@Override
public InputStream getInputStream() throws IOException {
try {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
this.keyStore.store(stream, new char[0]);
return new ByteArrayInputStream(stream.toByteArray());
}
catch (Exception ex) {
throw new IOException(ex);
}
}
}
}

@ -0,0 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.mtls.MtlsAutoConfiguration,\
com.alibaba.cloud.mtls.endpoint.MtlsEndpointAutoConfiguration
org.springframework.context.ApplicationListener=\
com.alibaba.cloud.mtls.server.PreparedEventRecorder

@ -0,0 +1,50 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import com.alibaba.cloud.governance.istio.XdsConfigProperties;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.governance.istio.sds.CertPair;
import com.alibaba.cloud.governance.istio.util.CertificateUtil;
public class MockCertManager extends AbstractCertManager {
public MockCertManager(XdsConfigProperties xdsConfigProperties) {
super(xdsConfigProperties);
}
public MockCertManager() {
super(null);
}
@Override
public synchronized CertPair getCertPair() {
Certificate[] certificate = CertificateUtil.loadCertificateFromPath(
MockCertManager.class.getResource("/cert/mtls.crt").getPath());
PrivateKey privateKey = CertificateUtil.loadPrivateKeyFromPath(
MockCertManager.class.getResource("/cert/mtls.key").getPath());
CertPair p = new CertPair();
p.setCertificateChain(certificate);
p.setPrivateKey(privateKey);
p.setExpireTime(Long.MAX_VALUE);
return p;
}
}

@ -0,0 +1,123 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import com.alibaba.cloud.commons.governance.ControlPlaneInitedBean;
import com.alibaba.cloud.governance.istio.XdsAutoConfiguration;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.mtls.client.MtlsClientSSLContext;
import com.alibaba.cloud.mtls.server.netty.MtlsNettyServerCustomizer;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.SslInfo;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.main.web-application-type=reactive",
"spring.cloud.nacos.discovery.enabled=false" },
classes = { MtlsClientTests.TestConfig.class })
public class MtlsClientTests {
@Value("${local.server.port}")
private int port;
private static CloseableHttpClient client;
private static CloseableHttpClient noCertClient;
@Autowired
private MtlsClientSSLContext context;
public void init() {
if (client == null) {
client = NoVerifyHttpClientFactory.getClient(context.getSslContext());
}
if (noCertClient == null) {
noCertClient = NoVerifyHttpClientFactory.getClient();
}
}
@Test
public void testStatusCode() throws Exception {
init();
CloseableHttpResponse response = client
.execute(new HttpGet("https://127.0.0.1:" + port + "/echo"));
Assert.assertEquals(response.getStatusLine().getStatusCode(), 200);
response = noCertClient
.execute(new HttpGet("https://127.0.0.1:" + port + "/echo"));
Assert.assertEquals(response.getStatusLine().getStatusCode(), 500);
}
@Configuration
@EnableAutoConfiguration(exclude = XdsAutoConfiguration.class)
@ImportAutoConfiguration(MtlsAutoConfiguration.class)
public static class TestConfig {
@Bean
public AbstractCertManager mockCertManager() {
return new MockCertManager();
}
@Bean
public ControlPlaneInitedBean mockInitBean() {
return new ControlPlaneInitedBean();
}
@Bean
public NettyReactiveWebServerFactory nettyFactory(
MtlsNettyServerCustomizer customizer) {
NettyReactiveWebServerFactory nettyReactiveWebServerFactory = new NettyReactiveWebServerFactory();
nettyReactiveWebServerFactory.addServerCustomizers(customizer);
return nettyReactiveWebServerFactory;
}
@RestController
public static class EchoController {
@RequestMapping("/echo")
public String echo(ServerWebExchange exchange) {
if (exchange.getRequest().getSslInfo() != null) {
SslInfo sslInfo = exchange.getRequest().getSslInfo();
if (sslInfo.getPeerCertificates() != null) {
return "success";
}
}
throw new RuntimeException("No client certificate");
}
}
}
}

@ -0,0 +1,76 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import com.alibaba.cloud.commons.governance.ControlPlaneInitedBean;
import com.alibaba.cloud.governance.istio.XdsAutoConfiguration;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.main.web-application-type=servlet",
"spring.cloud.nacos.discovery.enabled=false" },
classes = { MtlsClientTests.TestConfig.class })
public class MtlsMvcServerTests {
@Value("${local.server.port}")
private int port;
private CloseableHttpClient client = NoVerifyHttpClientFactory.getClient();
@Test
public void testStatusCode() throws Exception {
boolean flag = false;
CloseableHttpResponse response = client
.execute(new HttpGet("http://127.0.0.1:" + port + "/echo"));
Assert.assertEquals(400, response.getStatusLine().getStatusCode());
response = client.execute(new HttpGet("https://127.0.0.1:" + port + "/echo"));
Assert.assertEquals(404, response.getStatusLine().getStatusCode());
}
@Configuration
@EnableAutoConfiguration(exclude = XdsAutoConfiguration.class)
@ImportAutoConfiguration(MtlsAutoConfiguration.class)
public static class TestConfig {
@Bean
public AbstractCertManager mockCertManager() {
return new MockCertManager();
}
@Bean
public ControlPlaneInitedBean mockInitBean() {
return new ControlPlaneInitedBean();
}
}
}

@ -0,0 +1,103 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import com.alibaba.cloud.commons.governance.ControlPlaneInitedBean;
import com.alibaba.cloud.governance.istio.XdsAutoConfiguration;
import com.alibaba.cloud.governance.istio.sds.AbstractCertManager;
import com.alibaba.cloud.mtls.server.netty.MtlsNettyServerCustomizer;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.main.web-application-type=reactive",
"spring.cloud.nacos.discovery.enabled=false" },
classes = { MtlsWebfluxServerTest.TestConfig.class })
public class MtlsWebfluxServerTest {
@Value("${local.server.port}")
private int port;
private CloseableHttpClient client = NoVerifyHttpClientFactory.getClient();
@Test
public void testStatusCode() throws Exception {
boolean flag = false;
CloseableHttpResponse response;
try {
response = client.execute(new HttpGet("http://127.0.0.1:" + port + "/echo"));
}
catch (Throwable t) {
flag = true;
}
Assert.assertTrue(flag);
response = client.execute(new HttpGet("https://127.0.0.1:" + port + "/echo"));
Assert.assertEquals(response.getStatusLine().getStatusCode(), 200);
}
@Configuration
@EnableAutoConfiguration(exclude = XdsAutoConfiguration.class)
@ImportAutoConfiguration(MtlsAutoConfiguration.class)
public static class TestConfig {
@Bean
public AbstractCertManager mockCertManager() {
return new MockCertManager();
}
@Bean
public ControlPlaneInitedBean mockInitBean() {
return new ControlPlaneInitedBean();
}
@Bean
public NettyReactiveWebServerFactory nettyFactory(
MtlsNettyServerCustomizer customizer) {
NettyReactiveWebServerFactory nettyReactiveWebServerFactory = new NettyReactiveWebServerFactory();
nettyReactiveWebServerFactory.addServerCustomizers(customizer);
return nettyReactiveWebServerFactory;
}
@RestController
public static class EchoController {
@RequestMapping("/echo")
public String echo() {
return "echo";
}
}
}
}

@ -0,0 +1,95 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.mtls;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
public final class NoVerifyHttpClientFactory {
private NoVerifyHttpClientFactory() {
}
private static final CloseableHttpClient CLIENT;
static {
try {
CLIENT = HttpClients.custom()
.setSSLSocketFactory(new SSLConnectionSocketFactory(
createIgnoreVerifySSL(), null, null, new HostnameVerifier() {
@Override
public boolean verify(String hostname,
SSLSession session) {
return true;
}
}))
.build();
}
catch (Throwable t) {
throw new RuntimeException(t);
}
}
public static CloseableHttpClient getClient() {
return CLIENT;
}
public static CloseableHttpClient getClient(SSLContext sslContext) {
return HttpClients.custom()
.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext, null,
null, new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}))
.build();
}
private static SSLContext createIgnoreVerifySSL() throws Exception {
SSLContext sc = SSLContext.getInstance("TLS");
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
String paramString) {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
String paramString) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sc.init(null, new TrustManager[] { trustManager }, null);
return sc;
}
}

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICyTCCAbGgAwIBAgIEQDwBNzANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwpj
c2Iub25saW5lMB4XDTIyMTAxOTA5MzY0MFoXDTIzMDExNzA5MzY0MFowFTETMBEG
A1UEAxMKY3NiLm9ubGluZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AIFNp9P+w2Oxg7jtXKRWEBwHvQDzIxoORPARi9rHZDtJbvUPm9AQ+4RG59J1UV5t
Bf6HwGBQHOTeN5wVP40VZCPLPsyWFbptWk9DHpleW6RkV8GWvEf7rsecrXG/aO1t
sT7+L6P3XAdsxeSjXgvXr3+kjWz9AiVwLPdZuY++55ZUOnykeHdpqmedraN0N7Q8
M6JpjDn+KFnk16kqgIl5f0Z56iPeKmfu7JFE+yX9glwsrfwHeokgS93GKhOo9HLJ
KBaY7/LobCnzUxl7meOy02PAA7bMjI2peL0H/kc8yW7o6dtKIMt+wRk0d4Jfmzqh
fNNCNCnhEhiGTrCNKnt50h0CAwEAAaMhMB8wHQYDVR0OBBYEFA3VC9jg2HmhlSG0
13Idl3qa8KMLMA0GCSqGSIb3DQEBCwUAA4IBAQBas1jqiZEmyb36VQqgMtzu8fvt
wsQCE1D751MejDz0lMSNVd/Ze48OttxkmTQY0Qvf9B4NUMktZbDgqg5Gg9OSBTPM
RJGw4/e2J5VGydmNPGVGzJWQnrCpYWrZvv/tQCja7Y4d45PJSC4zsvTX5D8zjMdD
u/MI+K6BmYvktlE+TOk3jr2KahgrZFdXY1vV25jAmZbRksCswrbq6bwdUCjZuGG2
euTPI9JAEJqOGg+cniIoBQiIBjMnoeZUcO5Rmbm8iYtOMb6skmN8FUEIYxbr6xuw
Q8wa2AfKUrRwAWQAp9HMqj8xo42Iwq+eIxpwhZEGXS+vM3dqovISDNrVRjyT
-----END CERTIFICATE-----

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCBTafT/sNjsYO4
7VykVhAcB70A8yMaDkTwEYvax2Q7SW71D5vQEPuERufSdVFebQX+h8BgUBzk3jec
FT+NFWQjyz7MlhW6bVpPQx6ZXlukZFfBlrxH+67HnK1xv2jtbbE+/i+j91wHbMXk
o14L169/pI1s/QIlcCz3WbmPvueWVDp8pHh3aapnna2jdDe0PDOiaYw5/ihZ5Nep
KoCJeX9Geeoj3ipn7uyRRPsl/YJcLK38B3qJIEvdxioTqPRyySgWmO/y6Gwp81MZ
e5njstNjwAO2zIyNqXi9B/5HPMlu6OnbSiDLfsEZNHeCX5s6oXzTQjQp4RIYhk6w
jSp7edIdAgMBAAECggEABz2oBlEf8/c+3x47r+A56rfsN59l+dGOvrrc0BWWYVON
aR/Eo/QvgYrO9JQpSFZmnYhQl5Qk6hQfnRf7hRULlI2PKLnG4be59PJXRlNoYl5U
I70jMgzADuGRPOtxHsqjwFlPpaj2eFv5AQK9A4DCjS1T1iSc+Ce9/OQDZi9UnLBq
IJF8+zBDCgihpaGxlMAJpSHol8UBsE2rC6UVh+CcpgN708gwFfmcUk9BZy+GlAuF
xTSbJWBWsbSUdf762QUb2I4er3TdAJKN+VYbfNHqsozQrJ73ME0NjOdrsF/pA9LF
Ki9sgThk/IcB7yKMiJ6Gs8B2fONxbPh/vNjIV7oPAQKBgQDX/YDc1mZ3CF8vs/ZL
tPfM5fLdibOLiKUNETF/VBnO7G4rdIlCKMaXympyV2Kq2euEWfR08Po94RMYuIIJ
CUvkrfjaLK0VvRxTJyTSFwQSrEMvVU4D0AyqqA4U7/18cOB9Jxz3+wnpDDg6fU10
t4nwn/FpFmFsmm0DtmzhL6eYYQKBgQCZQVz6TwigKzrjD8O7RwBuvwGDdo/8ka19
xZVQGpP34GJkMcWj4Cw3fD7cnxtYuX5H2xuB5gfqaLsCG/5DcU4MdyL/e3x04Knm
bIstt2EUY9DeU0oXgpVIJ+G8J2xnqD/haBpHCvzZ8IaZgTNgd3BStABrR0LwGe+r
bHzWcb1jPQKBgF2Z5WtOmo8BGMcCdTzvyueHy00IbL+OUwCrr8ifOKP8v+2jDgyW
wSFsvd6ZUg/6al8r4I2BFOEWFgGDjA7AcZxDbHGYJNYj4w+CCinlgYVaE6+Ch1GN
qr+WHqwiKLbx78cs2Rf6OZw+CGwIezWWiHe3yJWi9ktrTMzsFJkt3rJhAoGADYL3
wZHKPuTQ0kgHh7Fg1mK8rWx8kVX+p8INwfw143rC8fZ5aFNRUqr/l8/nR1FDUu58
ZF11gTMumacCKcwJh4vRaBjpBhzwncIgGy25v2R1e8R4Gc6Hfs8VVdNb+V+aEjNt
baoIVOah11LOxsiA/KmmB89GlYiT2tc7wmRQwDkCgYEAvy6sDfpBa0CBAWOe/xL+
Nk9VzJO+hb4eRV+o6BGOCiGtjbhMBlFDsNpqCIcg/zE8VM7xOuNTUFPiwL6Aqpjo
xq9Zy8RyIejoN9vcZ1hjClozMOhhm/24+ItSzuIj76BZJ3g7WJNM29kGqtURDO8m
5JqOo9UOZoRLSV95TmfUiMg=
-----END PRIVATE KEY-----

@ -16,6 +16,11 @@
<java.version>1.8</java.version>
<!-- Envoy Api Versions -->
<envoy-api.version>0.1.32</envoy-api.version>
<bouncycastle_version>1.70</bouncycastle_version>
<grpc.version>1.34.1</grpc.version>
<protobuf.version>3.14.0</protobuf.version>
<os-plugin.version>1.6.2</os-plugin.version>
<pb-plugin.version>0.6.1</pb-plugin.version>
</properties>
<dependencies>
@ -72,6 +77,11 @@
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bouncycastle_version}</version>
</dependency>
<!-- Only for unit test-->
<dependency>
@ -96,7 +106,47 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>${os-plugin.version}</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${pb-plugin.version}</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<skip>false</skip>
<useArgumentFile>true</useArgumentFile>
<checkStaleness>true</checkStaleness>
<staleMillis>10000</staleMillis>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,138 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.governance.istio;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import com.alibaba.cloud.governance.istio.constant.IstioConstants;
import com.alibaba.cloud.governance.istio.protocol.AbstractXdsProtocol;
import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest;
import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author musi
* @author <a href="liuziming@buaa.edu.cn"></a>
* @since 2.2.10-RC1 AggregateDiscoveryService is used to send xds request and handle xds
* response.
*/
public class AggregateDiscoveryService {
private static final Logger log = LoggerFactory
.getLogger(AggregateDiscoveryService.class);
private final Map<String, AbstractXdsProtocol> protocolMap = new HashMap<>();
private final Map<String, Set<String>> requestResource = new ConcurrentHashMap<>();
private StreamObserver<DiscoveryRequest> observer;
private final XdsConfigProperties xdsConfigProperties;
private final XdsChannel xdsChannel;
private final ScheduledExecutorService retry;
public AggregateDiscoveryService(XdsChannel xdsChannel,
XdsConfigProperties xdsConfigProperties) {
this.xdsChannel = xdsChannel;
this.xdsConfigProperties = xdsConfigProperties;
this.observer = xdsChannel.createDiscoveryRequest(new XdsObserver());
this.retry = Executors.newSingleThreadScheduledExecutor();
}
public void addProtocol(AbstractXdsProtocol abstractXdsProtocol) {
protocolMap.put(abstractXdsProtocol.getTypeUrl(), abstractXdsProtocol);
}
public void sendXdsRequest(String typeUrl, Set<String> resourceNames) {
requestResource.put(typeUrl, resourceNames);
DiscoveryRequest request = DiscoveryRequest.newBuilder()
.setNode(xdsChannel.getNode()).setTypeUrl(typeUrl)
.addAllResourceNames(resourceNames).build();
observer.onNext(request);
}
private void sendAckRequest(DiscoveryResponse response) {
Set<String> ackResource = requestResource.getOrDefault(response.getTypeUrl(),
new HashSet<>());
DiscoveryRequest request = DiscoveryRequest.newBuilder()
.setVersionInfo(response.getVersionInfo()).setNode(xdsChannel.getNode())
.addAllResourceNames(ackResource).setTypeUrl(response.getTypeUrl())
.setResponseNonce(response.getNonce()).build();
observer.onNext(request);
}
@PreDestroy
public void close() {
retry.shutdown();
}
private class XdsObserver implements StreamObserver<DiscoveryResponse> {
@Override
public void onNext(DiscoveryResponse discoveryResponse) {
String typeUrl = discoveryResponse.getTypeUrl();
if (xdsConfigProperties.isLogXds()) {
log.info("Receive notification from xds server, type: {}, size: {}",
typeUrl, discoveryResponse.getResourcesCount());
}
AbstractXdsProtocol protocol = protocolMap.get(typeUrl);
if (protocol == null) {
throw new UnsupportedOperationException("No protocol of type " + typeUrl);
}
List<?> responses = protocol.decodeXdsResponse(discoveryResponse);
sendAckRequest(discoveryResponse);
protocol.onResponseDecoded(responses);
}
@Override
public void onError(Throwable throwable) {
if (xdsConfigProperties.isLogXds()) {
log.error("Connect to xds server failed, reconnect after 3 seconds",
throwable);
}
requestResource.clear();
retry.schedule(() -> {
xdsChannel.restart();
observer = xdsChannel.createDiscoveryRequest(this);
sendXdsRequest(IstioConstants.CDS_URL, new HashSet<>());
log.info("Reconnecting to istio control plane!");
}, 3, TimeUnit.SECONDS);
}
@Override
public void onCompleted() {
log.info("Xds connect completed");
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save