Skip to content

Commit

Permalink
Merge pull request quarkusio#38608 from cescoffier/http-tls-reload
Browse files Browse the repository at this point in the history
Allow TLS certificate reloading for the HTTP server
  • Loading branch information
cescoffier authored Feb 7, 2024
2 parents 2fc23dc + 6ab3ced commit 95ac381
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 5 deletions.
18 changes: 17 additions & 1 deletion docs/src/main/asciidoc/http-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
*
* <p>
* 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.
*/
Expand Down Expand Up @@ -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.
*
* <p>
* The order of the key files must match the order of the certificates.
*/
@ConfigItem
Expand Down Expand Up @@ -167,4 +171,15 @@ public class CertificateConfig {
*/
@ConfigItem
public Optional<String> 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.
* <p>
* Also, the update can also occur when the TLS certificate is configured using paths (and not in-memory).
* <p>
* The reload period must be equal or greater than 30 seconds. If not set, the certificate will not be reloaded.
*/
@ConfigItem
public Optional<Duration> reloadPeriod;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Long> reloadingTasks = new CopyOnWriteArrayList<>();

public WebDeploymentVerticle(HttpServerOptions httpOptions, HttpServerOptions httpsOptions,
HttpServerOptions domainSocketOptions, LaunchMode launchMode,
Expand Down Expand Up @@ -1185,6 +1188,14 @@ public void handle(AsyncResult<HttpServer> 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);
Expand All @@ -1198,6 +1209,10 @@ public void handle(AsyncResult<HttpServer> event) {
@Override
public void stop(Promise<Void> stopFuture) {

for (Long id : reloadingTasks) {
vertx.cancelTimer(id);
}

final AtomicInteger remainingCount = new AtomicInteger(0);
if (httpServer != null) {
remainingCount.incrementAndGet();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -223,7 +224,7 @@ public static HttpServerOptions createSslOptionsForManagementInterface(Managemen
return serverOptions;
}

private static Optional<String> getCredential(Optional<String> password, Map<String, String> credentials,
public static Optional<String> getCredential(Optional<String> password, Map<String, String> credentials,
Optional<String> passwordKey) {
if (password.isPresent()) {
return password;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long>() {
@Override
public void handle(Long id) {

vertx.executeBlocking(new Callable<SSLOptions>() {
@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<SSLOptions, Future<Boolean>>() {
@Override
public Future<Boolean> apply(SSLOptions res) {
if (res != null) {
return server.updateSSLOptions(res);
} else {
return Future.succeededFuture(false);
}
}
})
.onComplete(new Handler<AsyncResult<Boolean>>() {
@Override
public void handle(AsyncResult<Boolean> 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<Path> keys = new ArrayList<>();
final List<Path> 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<Buffer> certBuffer = new ArrayList<>();
List<Buffer> 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;
}
}

0 comments on commit 95ac381

Please sign in to comment.