Skip to content

Commit

Permalink
Periodic TLS certificate reload
Browse files Browse the repository at this point in the history
Provide a way to periodically reload certificates from the file system and document how to implement your own reloader.
  • Loading branch information
cescoffier committed Jun 27, 2024
1 parent dd017d5 commit 10654d6
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 8 deletions.
61 changes: 61 additions & 0 deletions docs/src/main/asciidoc/tls-registry-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,64 @@ When the application starts, the TLS registry performs some checks to ensure the
- the CRLs are valid

If any of these checks fail, the application will fail to start.

== Reloading Certificates

The `TlsConfiguration` obtained from the `TLSConfigurationRegistry` includes a mechanism for reloading certificates.
The `reload` method refreshes the key stores and trust stores, typically by reloading them from the file system.

NOTE: The reload operation is not automatic and must be triggered manually. Additionally, the `TlsConfiguration` implementation must support reloading (which is the case for the configured certificate).

The `reload` method returns a `boolean` indicating whether the reload was successful.
A value of `true` means the reload operation was successful, not necessarily that there were updates to the certificates.

After a `TlsConfiguration` has been reloaded, servers and clients using this configuration may need to perform specific actions to apply the new certificates.
The recommended approach is to fire a CDI event (`CertificateReloadedEvent`) that servers and clients can listen to and make the necessary changes:

[source, java]
----
@Inject
TlsConfigurationRegistry registry;
public void reload() {
TlsConfiguration config = registry.get("name").orElseThrow();
if (config.reload()) {
event.fire(new CertificateReloadedEvent("name", config));
}
}
// In the server or client code
public void onReload(@Observes CertificateReloadedEvent event) {
if ("name".equals(event.getName())) {
server.updateSSLOptions(event.tlsConfiguration().getSSLOptions());
// Or update the SSLContext.
}
}
----

These APIs provide a way to implement custom certificate reloading.

=== Periodic reloading

The TLS registry does include a built-in mechanism for periodically checking the file system for changes and reloading the certificates.
You can configure periodic reloading of certificates using properties.
The `reload-period` property specifies the interval at which certificates are reloaded, and it will emit a `CertificateReloadedEvent`.

[source, properties]
----
quarkus.tls.reload-period=1h
quarkus.tls.key-store.pem.0.cert=tls.crt
quarkus.tls.key-store.pem.0.key=tls.key
----

For each named configuration, you can set a specific reload period:

[source, properties]
----
quarkus.tls.http.reload-period=30min
quarkus.tls.http.key-store.pem.0.cert=tls.crt
quarkus.tls.http.key-store.pem.0.key=tls.key
----

Remember that the impacted server and client may need to listen to the `CertificateReloadedEvent` to apply the new certificates.
This is automatically done for the Quarkus HTTP server (including the management interface if enabled).
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.tls.runtime.CertificateRecorder;
import io.quarkus.tls.runtime.config.TlsConfig;
import io.quarkus.vertx.deployment.VertxBuildItem;
Expand All @@ -22,10 +23,11 @@ public class CertificatesProcessor {
public TlsRegistryBuildItem initializeCertificate(
TlsConfig config, Optional<VertxBuildItem> vertx, CertificateRecorder recorder,
BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
List<TlsCertificateBuildItem> otherCertificates) {
List<TlsCertificateBuildItem> otherCertificates,
ShutdownContextBuildItem shutdown) {

if (vertx.isPresent()) {
recorder.validateCertificates(config, vertx.get().getVertx());
recorder.validateCertificates(config, vertx.get().getVertx(), shutdown);
}

for (TlsCertificateBuildItem certificate : otherCertificates) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkus.tls;

/**
* Event fired when a certificate is updated.
* <p>
* IMPORTANT: Consumers of this event should be aware that the event is fired from a blocking context (worker thread),
* and thus can perform blocking operations.
*
* @param name the name of the certificate (as configured in the configuration, {@code <default>} for the default certificate)
* @param tlsConfiguration the updated TLS configuration - the certificate has already been updated
*/
public record CertificateUpdatedEvent(String name, TlsConfiguration tlsConfiguration) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.function.Supplier;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
Expand All @@ -25,6 +26,7 @@
public class CertificateRecorder implements TlsConfigurationRegistry {

private final Map<String, TlsConfiguration> certificates = new ConcurrentHashMap<>();
private volatile TlsCertificateUpdater reloader;

/**
* Validate the certificate configuration.
Expand All @@ -35,7 +37,7 @@ public class CertificateRecorder implements TlsConfigurationRegistry {
* @param config the configuration
* @param vertx the Vert.x instance
*/
public void validateCertificates(TlsConfig config, RuntimeValue<Vertx> vertx) {
public void validateCertificates(TlsConfig config, RuntimeValue<Vertx> vertx, ShutdownContext shutdownContext) {
// Verify the default config
if (config.defaultCertificateConfig().isPresent()) {
verifyCertificateConfig(config.defaultCertificateConfig().get(), vertx.getValue(), TlsConfig.DEFAULT_NAME);
Expand All @@ -45,6 +47,15 @@ public void validateCertificates(TlsConfig config, RuntimeValue<Vertx> vertx) {
for (String name : config.namedCertificateConfig().keySet()) {
verifyCertificateConfig(config.namedCertificateConfig().get(name), vertx.getValue(), name);
}

shutdownContext.addShutdownTask(new Runnable() {
@Override
public void run() {
if (reloader != null) {
reloader.close();
}
}
});
}

public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String name) {
Expand All @@ -55,7 +66,7 @@ public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String
KeyStoreConfig keyStoreConfig = config.keyStore().get();
ks = verifyKeyStore(keyStoreConfig, vertx, name);
sni = keyStoreConfig.sni();
if (sni) {
if (sni && ks != null) {
try {
if (Collections.list(ks.keyStore.aliases()).size() <= 1) {
throw new IllegalStateException(
Expand All @@ -81,6 +92,14 @@ public void verifyCertificateConfig(TlsBucketConfig config, Vertx vertx, String
}

certificates.put(name, new VertxCertificateHolder(vertx, name, config, ks, ts));

// Handle reloading if needed
if (config.reloadPeriod().isPresent()) {
if (reloader == null) {
reloader = new TlsCertificateUpdater(vertx);
}
reloader.add(name, certificates.get(name), config.reloadPeriod().get());
}
}

public static KeyStoreAndKeyCertOptions verifyKeyStore(KeyStoreConfig config, Vertx vertx, String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.tls.runtime;

import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;

import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.spi.CDI;

import io.quarkus.tls.CertificateUpdatedEvent;
import io.quarkus.tls.TlsConfiguration;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;

/**
* A helper class that reload the TLS certificates at a configured interval.
* When the certificate is reloaded, a {@link CertificateUpdatedEvent} is fired.
*/
public class TlsCertificateUpdater {

private final Vertx vertx;
private final CopyOnWriteArrayList<Long> tasks;
private final Event<CertificateUpdatedEvent> event;

public TlsCertificateUpdater(Vertx vertx) {
this.vertx = vertx;
this.tasks = new CopyOnWriteArrayList<>();
this.event = CDI.current().getBeanManager().getEvent().select(CertificateUpdatedEvent.class);
}

public void close() {
for (Long task : tasks) {
vertx.cancelTimer(task);
}
tasks.clear();
}

public void add(String name, TlsConfiguration tlsConfiguration, Duration period) {
var id = vertx.setPeriodic(period.toMillis(), new Handler<Long>() {
@Override
public void handle(Long id) {
vertx.executeBlocking(new Callable<Void>() {
@Override
public Void call() {
// Reload is most probably a blocking operation as it needs to reload the certificate from the
// file system. Thus, it is executed in a blocking context.
// Then we fire the event. This is also potentially blocking, as the consumer are invoked on the
// same thread.
if (tlsConfiguration.reload()) {
event.fire(new CertificateUpdatedEvent(name, tlsConfiguration));
}
return null;
}
}, false);
}
});

tasks.add(id);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Set;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.tls.CertificateUpdatedEvent;
import io.smallrye.config.WithDefault;

@ConfigGroup
Expand Down Expand Up @@ -106,4 +107,19 @@ public interface TlsBucketConfig {
*/
Optional<String> hostnameVerificationAlgorithm();

/**
* When configured, the server will reload the certificates (from the file system for example) and fires a
* {@link CertificateUpdatedEvent} if the reload is successful
* <p>
* This property configures the period to reload the certificates. IF not set, the certificates won't be reloaded
* automatically.
* However, the application can still trigger the reload manually using the {@link io.quarkus.tls.TlsConfiguration#reload()}
* method,
* and then fire the {@link CertificateUpdatedEvent} manually.
* <p>
* The fired event is used to notify the application that the certificates have been updated, and thus proceed with the
* actual switch of certificates.
*/
Optional<Duration> reloadPeriod();

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import io.quarkus.vertx.http.runtime.CurrentRequestProducer;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCertificateUpdateEventListener;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.VertxConfigBuilder;
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
Expand Down Expand Up @@ -171,6 +172,7 @@ AdditionalBeanBuildItem additionalBeans() {
.setUnremovable()
.addBeanClass(CurrentVertxRequest.class)
.addBeanClass(CurrentRequestProducer.class)
.addBeanClass(HttpCertificateUpdateEventListener.class)
.build();
}

Expand Down
Loading

0 comments on commit 10654d6

Please sign in to comment.