diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc
index a59a456d99a51..22c9db0150bcc 100644
--- a/docs/src/main/asciidoc/http-reference.adoc
+++ b/docs/src/main/asciidoc/http-reference.adoc
@@ -159,7 +159,7 @@ In both cases, a password must be provided. See the designated paragraph for a d
[TIP]
====
-To enable SSL support with native executables, please refer to our xref:native-and-ssl.adoc[Using SSL With Native Executables guide].
+To enable TLS/SSL support with native executables, please refer to our xref:native-and-ssl.adoc[Using SSL With Native Executables guide].
====
=== Providing a certificate and key file
@@ -232,6 +232,22 @@ values:
NOTE: if you use `redirect` or `disabled` and have not added an SSL certificate or keystore, your server will not start!
+=== Reloading the certificates
+
+Key store, trust store and certificate files can be reloaded periodically.
+Configure the `quarkus.http.ssl.certificate.reload-period` property to specify the interval at which the certificates should be reloaded:
+
+[source, properties]
+----
+quarkus.http.ssl.certificate.files=/mount/certs/cert.pem
+quarkus.http.ssl.certificate.key-files=/mount/certs/key.pem
+quarkus.http.ssl.certificate.reload-period=1h
+----
+
+The files are reloaded from the same location as they were initially loaded from.
+If there is no content change, the reloading is a no-op.
+It the reloading fails, the server will continue to use the previous certificates.
+
== Additional HTTP Headers
To enable HTTP headers to be sent on every response, add the following properties:
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
index 774fac0217c94..74b8668ded172 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java
@@ -1,9 +1,13 @@
package io.quarkus.vertx.http.runtime;
import java.nio.file.Path;
+import java.time.Duration;
import java.util.List;
import java.util.Optional;
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+import io.quarkus.credentials.CredentialsProvider;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConvertWith;
@@ -20,7 +24,7 @@ public class CertificateConfig {
* The {@linkplain CredentialsProvider}.
* If this property is configured, then a matching 'CredentialsProvider' will be used
* to get the keystore, keystore key, and truststore passwords unless these passwords have already been configured.
- *
+ *
* Please note that using MicroProfile {@linkplain ConfigSource} which is directly supported by Quarkus Configuration
* should be preferred unless using `CredentialsProvider` provides for some additional security and dynamism.
*/
@@ -51,7 +55,7 @@ public class CertificateConfig {
/**
* The list of path to server certificates private key files using the PEM format.
* Specifying multiple files requires SNI to be enabled.
- *
+ *
* The order of the key files must match the order of the certificates.
*/
@ConfigItem
@@ -167,4 +171,15 @@ public class CertificateConfig {
*/
@ConfigItem
public Optional trustStoreCertAlias;
+
+ /**
+ * When set, the configured certificate will be reloaded after the given period.
+ * Note that the certificate will be reloaded only if the file has been modified.
+ *
+ * Also, the update can also occur when the TLS certificate is configured using paths (and not in-memory).
+ *
+ * The reload period must be equal or greater than 30 seconds. If not set, the certificate will not be reloaded.
+ */
+ @ConfigItem
+ public Optional reloadPeriod;
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java
index b5ef4363ad125..c399eb4b2fa6a 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java
@@ -60,6 +60,7 @@
import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration;
import io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers;
import io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils;
+import io.quarkus.vertx.http.runtime.options.TlsCertificateReloadUtils;
import io.smallrye.common.vertx.VertxContext;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
@@ -765,6 +766,7 @@ private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTime
if (deploymentIdIfAny != null) {
VertxCoreRecorder.setWebDeploymentId(deploymentIdIfAny);
}
+
closeTask = new Runnable() {
@Override
public synchronized void run() {
@@ -1013,6 +1015,7 @@ private static class WebDeploymentVerticle extends AbstractVerticle implements R
private final HttpConfiguration.InsecureRequests insecureRequests;
private final HttpConfiguration quarkusConfig;
private final AtomicInteger connectionCount;
+ private final List reloadingTasks = new CopyOnWriteArrayList<>();
public WebDeploymentVerticle(HttpServerOptions httpOptions, HttpServerOptions httpsOptions,
HttpServerOptions domainSocketOptions, LaunchMode launchMode,
@@ -1185,6 +1188,14 @@ public void handle(AsyncResult event) {
portSystemProperties.set(schema, actualPort, launchMode);
}
+ if (https && quarkusConfig.ssl.certificate.reloadPeriod.isPresent()) {
+ long l = TlsCertificateReloadUtils.handleCertificateReloading(
+ vertx, httpsServer, httpsOptions, quarkusConfig);
+ if (l != -1) {
+ reloadingTasks.add(l);
+ }
+ }
+
if (remainingCount.decrementAndGet() == 0) {
//make sure we only complete once
startFuture.complete(null);
@@ -1198,6 +1209,10 @@ public void handle(AsyncResult event) {
@Override
public void stop(Promise stopFuture) {
+ for (Long id : reloadingTasks) {
+ vertx.cancelTimer(id);
+ }
+
final AtomicInteger remainingCount = new AtomicInteger(0);
if (httpServer != null) {
remainingCount.incrementAndGet();
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
index 2895ca2149df0..d53c444263d20 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java
@@ -88,6 +88,7 @@ public static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeCo
if (!certificates.isEmpty() && !keys.isEmpty()) {
createPemKeyCertOptions(certificates, keys, serverOptions);
} else if (keyStoreFile.isPresent()) {
+
KeyStoreOptions options = createKeyStoreOptions(
keyStoreFile.get(),
keyStorePassword.orElse("password"),
@@ -223,7 +224,7 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen
return serverOptions;
}
- private static Optional getCredential(Optional password, Map credentials,
+ public static Optional getCredential(Optional password, Map credentials,
Optional passwordKey) {
if (password.isPresent()) {
return password;
@@ -369,7 +370,7 @@ private static KeyStoreOptions createKeyStoreOptions(Path path, String password,
return options;
}
- private static byte[] getFileContent(Path path) throws IOException {
+ static byte[] getFileContent(Path path) throws IOException {
byte[] data;
final InputStream resource = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(ClassPathUtils.toResourceName(path));
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
new file mode 100644
index 0000000000000..2acf224b5d5c8
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/TlsCertificateReloadUtils.java
@@ -0,0 +1,136 @@
+package io.quarkus.vertx.http.runtime.options;
+
+import static io.quarkus.vertx.http.runtime.options.HttpServerOptionsUtils.getFileContent;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.vertx.http.runtime.HttpConfiguration;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.KeyStoreOptions;
+import io.vertx.core.net.PemKeyCertOptions;
+import io.vertx.core.net.SSLOptions;
+
+/**
+ * Utility class to handle TLS certificate reloading.
+ */
+public class TlsCertificateReloadUtils {
+
+ public static long handleCertificateReloading(Vertx vertx, HttpServer server,
+ HttpServerOptions options, HttpConfiguration configuration) {
+ // Validation
+ if (configuration.ssl.certificate.reloadPeriod.isEmpty()) {
+ return -1;
+ }
+ if (configuration.ssl.certificate.reloadPeriod.get().toMillis() < 30_000) {
+ throw new IllegalArgumentException(
+ "Unable to configure TLS reloading - The reload period cannot be less than 30 seconds");
+ }
+ if (options == null) {
+ throw new IllegalArgumentException("Unable to configure TLS reloading - The HTTP server options were not provided");
+ }
+ SSLOptions ssl = options.getSslOptions();
+ if (ssl == null) {
+ throw new IllegalArgumentException("Unable to configure TLS reloading - TLS/SSL is not enabled on the server");
+ }
+
+ Logger log = Logger.getLogger(TlsCertificateReloadUtils.class);
+ return vertx.setPeriodic(configuration.ssl.certificate.reloadPeriod.get().toMillis(), new Handler() {
+ @Override
+ public void handle(Long id) {
+
+ vertx.executeBlocking(new Callable() {
+ @Override
+ public SSLOptions call() throws Exception {
+ // We are reading files - must be done on a worker thread.
+ var c = reloadFileContent(ssl, configuration);
+ if (c.equals(ssl)) { // No change, skip the update
+ return null;
+ }
+ return c;
+ }
+ }, true)
+ .flatMap(new Function>() {
+ @Override
+ public Future apply(SSLOptions res) {
+ if (res != null) {
+ return server.updateSSLOptions(res);
+ } else {
+ return Future.succeededFuture(false);
+ }
+ }
+ })
+ .onComplete(new Handler>() {
+ @Override
+ public void handle(AsyncResult ar) {
+ if (ar.failed()) {
+ log.error("Unable to reload the TLS certificate, keeping the current one.", ar.cause());
+ } else {
+ if (ar.result()) {
+ log.debug("TLS certificates updated");
+ }
+ // Not updated, no change.
+ }
+ }
+ });
+ }
+ });
+ }
+
+ private static SSLOptions reloadFileContent(SSLOptions ssl, HttpConfiguration configuration) throws IOException {
+ var copy = new SSLOptions(ssl);
+
+ final List keys = new ArrayList<>();
+ final List certificates = new ArrayList<>();
+
+ if (configuration.ssl.certificate.keyFiles.isPresent()) {
+ keys.addAll(configuration.ssl.certificate.keyFiles.get());
+ }
+ if (configuration.ssl.certificate.files.isPresent()) {
+ certificates.addAll(configuration.ssl.certificate.files.get());
+ }
+
+ if (!certificates.isEmpty() && !keys.isEmpty()) {
+ List certBuffer = new ArrayList<>();
+ List keysBuffer = new ArrayList<>();
+
+ for (Path p : certificates) {
+ byte[] cert = getFileContent(p);
+ certBuffer.add(Buffer.buffer(cert));
+ }
+ for (Path p : keys) {
+ byte[] key = getFileContent(p);
+ keysBuffer.add(Buffer.buffer(key));
+ }
+
+ PemKeyCertOptions opts = new PemKeyCertOptions()
+ .setCertValues(certBuffer)
+ .setKeyValues(keysBuffer);
+ copy.setKeyCertOptions(opts);
+ } else if (configuration.ssl.certificate.keyStoreFile.isPresent()) {
+ var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
+ opts.setValue(Buffer.buffer(getFileContent(configuration.ssl.certificate.keyStoreFile.get())));
+ copy.setKeyCertOptions(opts);
+ }
+
+ if (configuration.ssl.certificate.trustStoreFile.isPresent()) {
+ var opts = ((KeyStoreOptions) copy.getKeyCertOptions());
+ opts.setValue(Buffer.buffer(getFileContent(configuration.ssl.certificate.trustStoreFile.get())));
+ copy.setTrustOptions(opts);
+ }
+
+ return copy;
+ }
+}