Skip to content

Commit

Permalink
Merge pull request #41501 from cescoffier/tls-reload-certs
Browse files Browse the repository at this point in the history
Cert-Manager support and TLS periodic reload
  • Loading branch information
gastaldi authored Jun 29, 2024
2 parents 6fbcb0e + 32fc596 commit c20fa84
Show file tree
Hide file tree
Showing 13 changed files with 710 additions and 8 deletions.
174 changes: 174 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,177 @@ 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).

== Using Kubernetes secrets or cert-manager

When running in Kubernetes, you can use Kubernetes secrets to store the key stores and trust stores.

=== Using Kubernetes secrets

To use Kubernetes secrets, you need to create a secret with the key stores and trust stores.
Let's take the following secret as an example:

[source, yaml]
----
apiVersion: v1
data:
tls.crt: ...
tls.key: ...
kind: Secret
metadata:
name: my-certs
type: kubernetes.io/tls
----

The easiest way to uses these certificates is to mount the secret as a volume in the pod:

[source, yaml]
----
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: demo
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/managed-by: quarkus
name: demo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: demo
app.kubernetes.io/version: 1.0.0-SNAPSHOT
template:
metadata:
labels:
app.kubernetes.io/managed-by: quarkus
app.kubernetes.io/name: demo
app.kubernetes.io/version: 1.0.0-SNAPSHOT
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: ...
imagePullPolicy: IfNotPresent
name: demo
ports:
- containerPort: 8443 # Configure the port to be HTTPS
name: http
protocol: TCP
volumeMounts:
- mountPath: /certs
name: my-volume
volumes:
- name: my-volume
secret:
defaultMode: 0666 # Set the permissions, otherwise the pod may not be able to read the files
optional: false
secretName: my-certs # Reference the secret
----

Then, you can configure the TLS registry to use the certificates:

[source, properties]
----
# ...
# TLS Registry configuration
%prod.quarkus.tls.http.key-store.pem.0.cert=/certs/tls.crt
%prod.quarkus.tls.http.key-store.pem.0.key=/certs/tls.key
# HTTP server configuration:
%prod.quarkus.http.tls-configuration-name=http
%prod.quarkus.http.insecure-requests=disabled
----

You can combine this with the periodic reloading to automatically reload the certificates when they change.

=== Using cert-manager

When running in Kubernetes, you can use cert-manager to automatically generate and renew certificates.
Cert-manager will produce a secret with the key stores and trust stores.
So, configuring the TLS registry is the same as when using Kubernetes secrets.
The generated secret uses the following files:

- `tls.crt` for the certificate
- `tls.key` for the private key
- `ca.crt` for the CA certificate (if needed)

To handle the renewal, you can use the periodic reloading mechanism:

[source, properties]
----
# ...
# TLS Registry configuration
%prod.quarkus.tls.http.key-store.pem.0.cert=/certs/tls.crt
%prod.quarkus.tls.http.key-store.pem.0.key=/certs/tls.key
%prod.quarkus.tls.http.reload-period=24h
# HTTP server configuration:
%prod.quarkus.http.tls-configuration-name=http
%prod.quarkus.http.insecure-requests=disabled
----

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 c20fa84

Please sign in to comment.