Skip to content

Commit

Permalink
Allow TLS certificate reloading for the HTTP server
Browse files Browse the repository at this point in the history
Key store, trust store and certificate files can be reloaded periodically.
The period is configured using the `quarkus.http.ssl.certificate.reload-period` property.

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.
  • Loading branch information
cescoffier committed Feb 6, 2024
1 parent cf7ae80 commit 1beea62
Show file tree
Hide file tree
Showing 5 changed files with 185 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].

Check warning on line 162 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.CaseSensitiveTerms] Use 'SSL/TLS' rather than 'SSL'. Raw Output: {"message": "[Quarkus.CaseSensitiveTerms] Use 'SSL/TLS' rather than 'SSL'.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 162, "column": 15}}}, "severity": "INFO"}
====

=== 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!

Check warning on line 233 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.CaseSensitiveTerms] Use 'SSL/TLS' rather than 'SSL'. Raw Output: {"message": "[Quarkus.CaseSensitiveTerms] Use 'SSL/TLS' rather than 'SSL'.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 233, "column": 43}}}, "severity": "INFO"}

=== Reloading the certificates

Check warning on line 235 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'truststore' rather than 'trust store' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'truststore' rather than 'trust store' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 235, "column": 22}}}, "severity": "WARNING"}

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:

Check warning on line 238 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 238, "column": 73}}}, "severity": "INFO"}

[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.

Check warning on line 247 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 247, "column": 25}}}, "severity": "INFO"}
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.

Check warning on line 249 in docs/src/main/asciidoc/http-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Additional HTTP Headers'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Additional HTTP Headers'.", "location": {"path": "docs/src/main/asciidoc/http-reference.adoc", "range": {"start": {"line": 249, "column": 66}}}, "severity": "INFO"}

== 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 greater than 1 second.
*/
@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,133 @@
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() < 1000) {
throw new IllegalArgumentException(
"Unable to configure TLS reloading - The reload period cannot be less than 1 second");
}
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(), 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 1beea62

Please sign in to comment.