Using SSL With Native Executables

我们正在迅速发展到一个无处不在的 SSL 世界,因此能够使用 SSL 至关重要。

We are quickly moving to an SSL-everywhere world so being able to use SSL is crucial.

在本指南中,我们将讨论如何让你的本机可执行文件支持 SSL,因为本机可执行文件开箱即用时并不支持 SSL。

In this guide, we will discuss how you can get your native executables to support SSL, as native executables don’t support it out of the box.

如果你不打算使用本机可执行文件,你可以使用 JDK 模式,SSL 在这种模式下受支持,无需进一步操作。

If you don’t plan on using native executables, you can pass your way as in JDK mode, SSL is supported without further manipulations.

Prerequisites

如要完成本指南,您需要:

To complete this guide, you need:

  • less than 20 minutes

  • an IDE

  • GraalVM installed with JAVA_HOME and GRAALVM_HOME configured appropriately

  • Apache Maven {maven-version}

本指南基于 REST 客户端指南,因此你应该先获取此 Maven 项目。

This guide is based on the REST client guide, so you should get this Maven project first.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

该项目位于 resteasy-client-quickstart directory 中。

The project is located in the resteasy-client-quickstart directory.

Looks like it works out of the box?!?

如果你打开该应用程序的配置文件 (src/main/resources/application.properties),可以看到以下行:

If you open the application’s configuration file (src/main/resources/application.properties), you can see the following line:

quarkus.rest-client."org.acme.rest.client.ExtensionsService".url=https://stage.code.quarkus.io/api

它配置我们的 REST 客户端以连接到 SSL REST 服务。

which configures our REST client to connect to an SSL REST service.

出于本指南的目的,我们还需要删除启动嵌入式 WireMock 服务器(用于截取 REST 客户端响应,以便测试实际传播调用)的配置 [role="bare"][role="bare"]https://stage.code.quarkus.io/api。更新测试文件 src/test/java/org/acme/rest/client/ExtensionsResourceTest.java,并删除以下行:

For the purposes of this guide, we also need to remove the configuration that starts the embedded WireMock server that stubs REST client responses so the tests actually propagate calls to the [role="bare"]https://stage.code.quarkus.io/api. Update the test file src/test/java/org/acme/rest/client/ExtensionsResourceTest.java and remove the line:

@WithTestResource(WireMockExtensions.class)

来自 ExtensionsResourceTest 类。

from the ExtensionsResourceTest class.

现在,让我们将应用程序构建为本机可执行文件并运行测试:

Now let’s build the application as a native executable and run the tests:

Unresolved directive in native-and-ssl.adoc - include::{includes}/devtools/build-native.adoc[]

我们会获得以下结果:

And we obtain the following result:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

所以是的,它似乎可以开箱即用,本指南相当无用。

So, yes, it appears it works out of the box and this guide is pretty useless.

但是,事实并非如此。构建本机可执行文件时会出现以下问题:

It’s not. The magic happens when building the native executable:

[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=3 -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Duser.language=en -J-Duser.country=IE -J-Dfile.encoding=UTF-8 --features=io.quarkus.runner.Feature,io.quarkus.runtime.graal.ResourcesFeature,io.quarkus.runtime.graal.DisableLoggingFeature -J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED -J--add-opens=java.base/java.text=ALL-UNNAMED -J--add-opens=java.base/java.io=ALL-UNNAMED -J--add-opens=java.base/java.lang.invoke=ALL-UNNAMED -J--add-opens=java.base/java.util=ALL-UNNAMED -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+AllowFoldMethods -J-Djava.awt.headless=true -H:FallbackThreshold=0 --link-at-build-time -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http,https -H:NativeLinkerOption=-no-pie -H:-UseServiceLoaderFeature -H:+StackTrace -J--add-exports=org.graalvm.sdk/org.graalvm.nativeimage.impl=ALL-UNNAMED -J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED -J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.configure=ALL-UNNAMED -J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.proxy=ALL-UNNAMED -J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.localization=ALL-UNNAMED rest-client-quickstart-1.0.0-SNAPSHOT-runner -jar rest-client-quickstart-1.0.0-SNAPSHOT-runner.jar

重要部分是 Quarkus 自动添加的以下选项:

The important part is the following option that was automatically added by Quarkus:

-H:EnableURLProtocols=http,https

它为您的本机可执行文件启用了本机 SSL 支持。但是您不应该手动设置它,我们为此目的提供了一个友好的配置属性,如以下所述。

It enables the native SSL support for your native executable. But you should not set it manually, we have a nice configuration property for this purpose as described below.

由于 SSL 如今实际上是标准,我们决定为以下某些扩展自动启用对其的支持:

As SSL is de facto the standard nowadays, we decided to enable its support automatically for some of our extensions:

  • the Agroal connection pooling extension (quarkus-agroal),

  • the Amazon Services extension (quarkus-amazon-*),

  • the Consul Config extension (quarkus-config-consul),

  • the Elasticsearch client extensions (quarkus-elasticsearch-rest-client and quarkus-elasticsearch-java-client) and thus the Hibernate Search Elasticsearch extension (quarkus-hibernate-search-orm-elasticsearch),

  • the Elytron Security OAuth2 extension (quarkus-elytron-security-oauth2),

  • the gRPC extension (quarkus-grpc),

  • the Infinispan Client extension (quarkus-infinispan-client).

  • the Jaeger extension (quarkus-jaeger),

  • the JGit extension (quarkus-jgit),

  • the JSch extension (quarkus-jsch),

  • the Kafka Client extension (quarkus-kafka-client), if Apicurio Registry 2.x Avro library is used

  • the Keycloak Authorization extension (quarkus-keycloak-authorization),

  • the Kubernetes client extension (quarkus-kubernetes-client),

  • the Logging Sentry extension (quarkus-logging-sentry),

  • the Mailer extension (quarkus-mailer),

  • the MongoDB client extension (quarkus-mongodb-client),

  • the Neo4j extension (quarkus-neo4j),

  • the OIDC and OIDC client extensions (quarkus-oidc and quarkus-oidc-client),

  • the Reactive client for IBM DB2 extension (quarkus-reactive-db2-client),

  • the Reactive client for PostgreSQL extension (quarkus-reactive-pg-client),

  • the Reactive client for MySQL extension (quarkus-reactive-mysql-client),

  • the Reactive client for Microsoft SQL Server extension (quarkus-reactive-mssql-client),

  • the Redis client extension (quarkus-redis-client),

  • the RESTEasy Classic REST Client extension (quarkus-resteasy-client),

  • the REST Client extension (quarkus-rest-client),

  • the SmallRye GraphQL Client extension (quarkus-smallrye-graphql-client),

  • the Spring Cloud Config client extension (quarkus-spring-cloud-config-client),

  • the Vault extension (quarkus-vault),

  • the Cassandra client extensions (cassandra-quarkus-client)

只要在项目中有一个这些扩展,则默认情况下会启用 SSL 支持。

As long as you have one of these extensions in your project, the SSL support will be enabled by default.

如果您不使用其中任何一个,而您仍希望启用 SSL 支持,请将以下内容添加到配置中:

If you are not using any of them and you want to enable SSL support anyway, please add the following to your configuration:

quarkus.ssl.native=true

现在,我们只检查本机可执行文件的大小,因为这在后面会用到:

Now, let’s just check the size of our native executable as it will be useful later:

$ ls -lh target/resteasy-client-quickstart-1.0.0-SNAPSHOT-runner
-rwxrwxr-x. 1 gandrian gandrian 46M Jun 11 13:01 target/rest-client-quickstart-1.0.0-SNAPSHOT-runner

Let’s disable SSL and see how it goes

Quarkus 有一个选项可以完全禁用 SSL 支持。为什么?因为它有一定的代价。因此,如果您确定不需要它,则可以完全禁用它。

Quarkus has an option to disable the SSL support entirely. Why? Because it comes at a certain cost. So if you are sure you don’t need it, you can disable it entirely.

首先,我们先禁用它,而不更改 REST 服务 URL,看看会怎样。

First, let’s disable it without changing the REST service URL and see how it goes.

打开 `src/main/resources/application.properties`并添加以下行:

Open src/main/resources/application.properties and add the following line:

quarkus.ssl.native=false

我们再试着构建:

And let’s try to build again:

Unresolved directive in native-and-ssl.adoc - include::{includes}/devtools/build-native.adoc[]

本机可执行文件测试将会失败,并出现以下错误:

The native executable tests will fail with the following error:

Caused by: java.lang.IllegalArgumentException: https://stage.code.quarkus.io/api requires SSL support but it is disabled. You probably have set quarkus.ssl.native to false.

当在没有在您的本机可执行文件中明确启用 SSL 而尝试使用 SSL 时,您将获得这个错误。

This error is the one you obtain when trying to use SSL while it was not explicitly enabled in your native executable.

现在,我们更改 REST 服务 URL 为 *not*在 `src/main/resources/application.properties`中使用 SSL:

Now, let’s change the REST service URL to not use SSL in src/main/resources/application.properties:

quarkus.rest-client."org.acme.rest.client.ExtensionsService".url=http://stage.code.quarkus.io/api

而且,由于 [role="bare"][role="bare"]http://stage.code.quarkus.io/api使用 302 状态码进行响应,我们需要使用 `-DskipTests`跳过测试。

and since [role="bare"]http://stage.code.quarkus.io/api responds with 302 status code we need to also skip the tests with -DskipTests.

现在我们可以再构建:

Now we can build again:

include::{includes}/devtools/build-native.adoc[]:!build-additional-parameters:

Unresolved directive in native-and-ssl.adoc - include::{includes}/devtools/build-native.adoc[] :!build-additional-parameters:

如果您仔细检查本机可执行文件构建选项,您会发现 SSL 相关选项已消失:

If you check carefully the native executable build options, you can see that the SSL related options are gone:

[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] /opt/graalvm/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dcom.sun.xml.internal.bind.v2.bytecode.ClassTailor.noOptimize=true -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -jar rest-client-1.0.0-SNAPSHOT-runner.jar -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 -H:+PrintAnalysisCallTree -H:EnableURLProtocols=http -H:-SpawnIsolates -H:+JNI --no-server -H:-UseServiceLoaderFeature -H:+StackTrace

结果为:

And we end up with:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

你是否记得我们之前检查了启用 SSL 时的本机可执行文件大小?让我们在完全禁用 SSL 支持的情况下再次检查:

You remember we checked the size of the native executable with SSL enabled? Let’s check again with SSL support entirely disabled:

$ ls -lh target/resteasy-client-quickstart-1.0.0-SNAPSHOT-runner
-rwxrwxr-x. 1 gandrian gandrian 35M Jun 11 13:06 target/resteasy-client-quickstart-1.0.0-SNAPSHOT-runner

是的,它现在是 35 MB,以前是 46 MB。SSL 在本机可执行文件大小中产生 11 MB 的开销。

Yes, it is now 35 MB whereas it used to be 46 MB. SSL comes with an 11 MB overhead in native executable size.

而且还有更多。

And there’s more to it.

Let’s start again with a clean slate

让我们还原我们对配置文件所做的更改,并使用以下命令返回到 SSL:

Let’s revert the changes we made to the configuration file and go back to SSL with the following command:

git checkout -- src/main/resources/application.properties

再编译本机可执行文件:

And let’s build the native executable again:

Unresolved directive in native-and-ssl.adoc - include::{includes}/devtools/build-native.adoc[]

The TrustStore path

这种做法对 GraalVM 21.3+ 是新引入的。

This behavior is new to GraalVM 21.3+.

GraalVM 支持构建时间和运行时证书配置。

GraalVM supports both build time and runtime certificate configuration.

Build time configuration

构建时间方法采用“不可变安全性” 的原则,其中适当的证书在构建时添加,之后永远不会被更改。这保证了在应用程序在生产环境中部署时,有效证书的列表不会被篡改。

The build time approach favors the principle of "immutable security" where the appropriate certificates are added at build time, and can never be changed afterward. This guarantees that the list of valid certificates cannot be tampered with when the application gets deployed in production.

然而,这也会带来一些缺点:

However, this comes with a few drawbacks:

  • If you use the same executable in all environments, and a certificate expires, the application needs to be rebuilt, and redeployed into production with the new certificate, which is an inconvenience.

  • Even worse, if a certificate gets revoked because of a security breach, all applications that embed this certificate need to be rebuilt and redeployed in a timely manner.

  • This requires also to add into the application all certificates for all environments (e.g. dev, test, prod), which means that a certificate that is required for dev mode but should not be used elsewhere, will make its way anyway in production.

  • Providing all certificates at build time complicates the CI, specifically in dynamic environments such as Kubernetes where valid certificates are provided by the platform in the /var/run/secrets/kubernetes.io/serviceaccount/ca.crt PEM file.

  • Lastly, this does not play well with third party software that do not provide a dedicated build for each customer environment.

使用构建时间证书创建本机可执行文件本质上意味着根证书在映像构建时被修复,基于构建时使用的证书配置(对于 Quarkus 来说,这意味着当你执行具有 `quarkus.native.enabled=true`设置的构建时)。这样做无需发送一个 `cacerts`文件或要求设置系统属性以便设置 OS 在其中运行二进制文件的提供的根证书。

Creating a native executable using build time certificates essentially means that the root certificates are fixed at image build time, based on the certificate configuration used at build time (which for Quarkus means when you perform a build having quarkus.native.enabled=true set). This avoids shipping a cacerts file or requiring a system property be set in order to set up root certificates that are provided by the OS where the binary runs.

在这种情况下,系统属性(如 javax.net.ssl.trustStore)在运行时不起作用,因此当需要更改默认值时,这些系统属性必须在映像构建时提供。这样做的最简单方法是设置 quarkus.native.additional-build-args。例如:

In this situation, system properties such as javax.net.ssl.trustStore do not have an effect at run time, so when the defaults need to be changed, these system properties must be provided at image build time. The easiest way to do so is by setting quarkus.native.additional-build-args. For example:

quarkus.native.additional-build-args=-J-Djavax.net.ssl.trustStore=/tmp/mycerts,-J-Djavax.net.ssl.trustStorePassword=changeit

将确保 `/tmp/mycerts`的证书被烘焙到本机二进制文件中,并使用默认的 `cacerts`的 instead。包含自定义信任库的文件 not(可能也不应该)在运行时存在,因为其内容已被烘焙到本机二进制文件中。

will ensure that the certificates of /tmp/mycerts are baked into the native binary and used instead of the default cacerts. The file containing the custom TrustStore does not (and probably should not) have to be present at runtime as its content has been baked into the native binary.

Run time configuration

从 GraalVM 21.3 开始支持的运行时证书配置与常规 Java 程序或 JVM 模式下的 Quarkus 相比,不需要任何特殊或额外的配置。有关更多信息,请阅读“本机映像中的 GraalVM 证书管理”指南的 Runtime Options部分。

Using the runtime certificate configuration, supported by GraalVM since 21.3 does not require any special or additional configuration compared to regular java programs or Quarkus in jvm mode. For more information, see the Runtime Options section of the "GraalVM Certificate Management in Native Image" guide.

Working with containers

在容器中运行本机二进制文件时无需采取特殊操作。如果本机二进制文件通过前一节中描述的自定义信任库正确构建,它还将在容器中正常工作。

No special action needs to be taken when running the native binary in a container. If the native binary was properly built with the custom TrustStore as described in the previous section, it will work properly in container as well.

Conclusion

我们通过 SSL 对本机可执行文件进行构建变得简单,并提供了多种选择以应对不同类型的安全需求。

We make building native executable using SSL easy, and provide several options to cope well with different types of security requirements.