Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cert-Manager support and TLS periodic reload #41501

Merged
merged 2 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
- 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.

Check warning on line 533 in docs/src/main/asciidoc/tls-registry-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 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 533, "column": 66}}}, "severity": "INFO"}

Check warning on line 533 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 533, "column": 92}}}, "severity": "WARNING"}

Check warning on line 533 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 533, "column": 96}}}, "severity": "INFO"}
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.

Check warning on line 562 in docs/src/main/asciidoc/tls-registry-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 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 562, "column": 53}}}, "severity": "INFO"}
The `reload-period` property specifies the interval at which certificates are reloaded, and it will emit a `CertificateReloadedEvent`.

Check warning on line 563 in docs/src/main/asciidoc/tls-registry-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/tls-registry-reference.adoc", "range": {"start": {"line": 563, "column": 55}}}, "severity": "INFO"}

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

Check warning on line 581 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possiblity)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 581, "column": 46}}}, "severity": "WARNING"}

Check warning on line 581 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 581, "column": 50}}}, "severity": "INFO"}
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.

Check warning on line 590 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 590, "column": 32}}}, "severity": "INFO"}
Let's take the following secret as an example:

Check warning on line 591 in docs/src/main/asciidoc/tls-registry-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/tls-registry-reference.adoc", "range": {"start": {"line": 591, "column": 33}}}, "severity": "INFO"}

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

Check warning on line 605 in docs/src/main/asciidoc/tls-registry-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/tls-registry-reference.adoc", "range": {"start": {"line": 605, "column": 67}}}, "severity": "INFO"}

[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();
cescoffier marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading