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; + } +}