了解如何在 Quarkus 应用中执行双向 TLS

在本篇文章中,我们将探讨如何在裸金属和 Kubernetes 上手动设置两个 Quarkus 应用之间的双向 TLS 加密。

什么是双向 TLS 认证?

双向 TLS 认证,也称为双向认证,是传输层安全(或“TLS”)的扩展,它确保客户端和服务器之间的流量在两个方向上都是安全和可信的。

为什么要使用双向 TLS?

它提供了一个额外的安全层,不仅验证服务器的身份,也验证客户端的身份。

如何实现?

在两个服务之间实现双向 TLS 的最佳方法是将其委托给基础设施,例如服务网格。这可以实现标准且安全的通信方式,并避免每个应用程序都实现自己的解决方案。但是,您并非总是在尖端环境中工作。

如果您没有 Istio 这样的服务网格环境,让我们看看如何在 Quarkus 中实现 mTLS。

MutualTLS Diagram

引导

让我们创建我们将要保护的服务器和客户端应用程序。

mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=quarkus-server-mtls \
    -DclassName="org.acme.server.mtls.GreetingResource" \
    -Dextensions="rest-client, resteasy-jsonb, kubernetes-client" \
    -Dpath="/hello-server"
mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=quarkus-client-mtls \
    -DclassName="org.acme.client.mtls.GreetingResource" \
    -Dextensions="rest-client, resteasy-jsonb, kubernetes-client" \
    -Dpath="/hello-client"

证书和信任库生成

当然,您需要服务器、客户端证书和信任库 :)

keytool -genkeypair -storepass password -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore quarkus-server-mtls/src/main/resources/META-INF/resources/server.keystore

keytool -genkeypair -storepass password -keyalg RSA -keysize 2048 -dname "CN=client" -alias client -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore quarkus-client-mtls/src/main/resources/META-INF/resources/client.keystore

在此示例中,我们使用以下方式模拟信任库:

  • client.keystore 作为服务器应用程序的信任库。

  • server.keystore 作为客户端应用程序的信任库。

cp quarkus-server-mtls/src/main/resources/META-INF/resources/server.keystore quarkus-client-mtls/src/main/resources/META-INF/resources/client.truststore

cp quarkus-client-mtls/src/main/resources/META-INF/resources/client.keystore quarkus-server-mtls/src/main/resources/META-INF/resources/server.truststore

Hello Server 应用

让我们打开并配置服务器 quarkus-server-mtls

在 Hello Server 应用中启用 SSL

将以下属性添加到您的应用程序 src/main/resources/application.properties 中以启用 SSL。

application.properties
quarkus.ssl.native=true

quarkus.http.ssl-port=8443
quarkus.http.ssl.certificate.key-store-file=META-INF/resources/server.keystore
quarkus.http.ssl.certificate.key-store-password=password

quarkus.http.port=0
quarkus.http.test-port=0
请参阅指南 使用原生 SSL,详细了解 SSL 在 Quarkus 中的工作原理。

激活客户端认证

application.properties
quarkus.http.ssl.client-auth=required
quarkus.http.ssl.certificate.trust-store-file=META-INF/resources/server.truststore
quarkus.http.ssl.certificate.trust-store-password=password

更新 GreetingResource

为了更好地看到响应来自服务器应用程序,让我们更新 org.acme.server.mtls.GreetingResource 类。

org.acme.server.mtls.GreetingResource
@Path("/hello-server")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello from server"; (1)
    }
}
1 将返回语句更改为“hello from server”。

还有测试类。

org.acme.server.mtls.GreetingResourceTest
@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello-server")
          .then()
             .statusCode(200)
             .body(is("hello from server")); (1)
    }

}
1 将匹配器更改为“hello from server”。

运行它

mvn quarkus:dev

如果一切正常,当您尝试访问 /hello-server 端点时,您应该会看到以下错误。

curl -k https://:8443/hello-server
curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate

这意味着您的客户端(curl)在连接到服务器时没有提供受信任的证书。

Hello Client 应用

此时,服务器应用程序已准备好实现双向 TLS。让我们打开并配置客户端 quarkus-client-mtls

为服务器应用添加 REST 客户端

org.acme.client.mtls.GreetingService
@Path("/")
@ApplicationScoped
@RegisterRestClient
public interface GreetingService {

    @GET
    @Path("/hello-server")
    @Produces(MediaType.TEXT_PLAIN)
    String hello();
}

org.acme.client.mtls.GreetingResource 中注入 GreetingService REST 客户端。

org.acme.client.mtls.GreetingResource
@Path("/hello-client")
public class GreetingResource {

    @Inject (1)
    @RestClient (2)
    GreetingService greetingService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return greetingService.hello(); (3)
    }
}
1 CDI @Inject 注解。
2 MicroProfile @RestClient 注解。
3 将返回语句替换为 greetingService.hello();
请参阅指南 rest-client,详细了解。

更新单元测试

quarkus-junit5-mockito 依赖添加到您的项目中。

pom.xml
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5-mockito</artifactId>
    </dependency>
org.acme.client.mtls.GreetingResourceTest
@QuarkusTest
public class GreetingResourceTest {

    @InjectMock (1)
    @RestClient (2)
    GreetingService greetingService;

    @Test
    public void testHelloEndpoint() {
        Mockito.when(greetingService.hello()).thenReturn("hello from server"); (3)

        given()
          .when().get("/hello-client")
          .then()
             .statusCode(200)
             .body(is("hello from server"));
    }

}
1 注入 CDI bean。
2 RestClient 类型。
3 模拟 hello 请求。
请参阅指南 Testing,详细了解。

为双向 TLS 配置 MicroProfile REST 客户端

将以下属性添加到您的应用程序 src/main/resources/application.properties 中以启用 SSL。

application.properties
org.acme.client.mtls.GreetingService/mp-rest/url=https://:8443
org.acme.client.mtls.GreetingService/mp-rest/trustStore=classpath:/META-INF/resources/client.truststore
org.acme.client.mtls.GreetingService/mp-rest/trustStorePassword=password
org.acme.client.mtls.GreetingService/mp-rest/keyStore=classpath:/META-INF/resources/client.keystore
org.acme.client.mtls.GreetingService/mp-rest/keyStorePassword=password

quarkus.ssl.native=true

运行它

mvn quarkus:dev

现在让我们访问客户端 /hello-client 端点,您应该会看到以下结果。

curl https://:8080/hello-client
hello from server

外部配置

您不希望在应用程序中包含证书,Quarkus 允许您使用外部配置并覆盖运行时应用程序属性。

让我们看看如何在 Kubernetes / OpenShift 中实现。

在应用程序引导过程中,您添加了 kubernetes-config 扩展。该扩展通过直接从 Kubernetes API 读取 ConfigMaps 来工作。

如果您处于一个受限制的环境中,不允许您的服务帐户查看 ConfigMap 的角色,您需要将外部 config 目录挂载到当前工作目录下,例如 <working-dir>/config/application.properties

工作目录对于

  • JVM redhat-openjdk-18/openjdk18-openshift 镜像是 /deployments

  • 原生 ubi-quarkus-native-s2i:19.3.1-java11 镜像是 /home/quarkus

volumeMounts 示例
# ...

      volumes:
        - name: config
          configMap:
              name: client

# ...
          volumeMounts:
            - name: config
              mountPath: /deployments

Secret

创建包含您的证书和信任库的服务器、客户端和信任库 secret。例如:

kubectl create secret generic server --from-file=tls/server/
kubectl create secret generic client --from-file=tls/client/
kubectl create secret generic truststore --from-file=tls/ca/truststore

ConfigMap

创建服务器和客户端 ConfigMap。

server-ConfigMap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: server
data:
  application.properties: |
    quarkus.http.ssl.certificate.key-store-file=/deployments/tls/server.keystore
    quarkus.http.ssl.certificate.key-store-password=password
    quarkus.http.ssl.certificate.trust-store-file=/deployments/tls/ca/truststore
    quarkus.http.ssl.certificate.trust-store-password=password
client-ConfigMap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: client
data:
  application.properties: |
    org.acme.client.mtls.GreetingService/mp-rest/url=https://server:8443
    org.acme.client.mtls.GreetingService/mp-rest/trustStore=/deployments/tls/ca/truststore
    org.acme.client.mtls.GreetingService/mp-rest/trustStorePassword=password
    org.acme.client.mtls.GreetingService/mp-rest/keyStore=/deployments/tls/client.keystore
    org.acme.client.mtls.GreetingService/mp-rest/keyStorePassword=password

在服务器和客户端应用程序中启用 kubernetes-config 扩展

为服务器应用程序添加以下属性。

application.properties
# only when running in prod (Kubernetes environment)
%prod.quarkus.kubernetes-config.enabled=true

quarkus.kubernetes-config.config-maps=server

以及客户端应用程序。

application.properties
# only when running in prod (Kubernetes environment)
%prod.quarkus.kubernetes-config.enabled=true

quarkus.kubernetes-config.config-maps=client
请参阅指南 Kubernetes Config,详细了解。

部署所有内容

以下是客户端应用程序的示例。

kind: Deployment
apiVersion: apps/v1
metadata:
  name: client
  namespace: mtls
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      volumes:
        - name: client
          secret:
            secretName: client
        - name: truststore
          secret:
            secretName: truststore
      containers:
        - name: client
          image: 'image-registry.openshift-image-registry.svc:5000/mtls/client:latest'
          ports:
            - containerPort: 8443
              protocol: TCP
          resources: {}
          volumeMounts:
            - name: client
              readOnly: true
              mountPath: /deployments/tls
            - name: truststore
              readOnly: true
              mountPath: /deployments/tls/ca

结论

您已成功在 Quarkus 中实现了双向 TLS。完整的 Quarkus 双向 TLS 示例可在上述链接中提到的 GitHub 存储库中找到。