Add a MeterBinder for SSL chain expiry

It registers a 'ssl.chains' gauge to count the number of chains with
different statuses (valid, expired, not yet valid, will expire soon).

Additionally, it registers a 'ssl.chain.expiry' gauge for every
certificate in a chain, tracking the seconds until expiry.

This binder reacts on bundle updates and new bundle registrations.

Closes gh-42030
This commit is contained in:
Moritz Halbritter 2024-10-21 15:01:27 +02:00
parent 4685fdebf0
commit 02a49b6038
10 changed files with 592 additions and 3 deletions

View File

@ -0,0 +1,272 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.ssl;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MultiGauge;
import io.micrometer.core.instrument.MultiGauge.Row;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.boot.info.SslInfo;
import org.springframework.boot.info.SslInfo.BundleInfo;
import org.springframework.boot.info.SslInfo.CertificateChainInfo;
import org.springframework.boot.info.SslInfo.CertificateInfo;
import org.springframework.boot.info.SslInfo.CertificateValidityInfo;
import org.springframework.boot.info.SslInfo.CertificateValidityInfo.Status;
import org.springframework.boot.ssl.SslBundles;
/**
* {@link MeterBinder} which registers the SSL chain validity (soonest to expire
* certificate in the chain) as a {@link TimeGauge}. Also contributes two {@link Gauge
* gauges} to count the valid and invalid chains.
*
* @author Moritz Halbritter
*/
class SslMeterBinder implements MeterBinder {
private static final String CHAINS_METRIC_NAME = "ssl.chains";
private static final String CHAIN_EXPIRY_METRIC_NAME = "ssl.chain.expiry";
private final Clock clock;
private final SslInfo sslInfo;
private final BundleMetrics bundleMetrics = new BundleMetrics();
SslMeterBinder(SslInfo sslInfo, SslBundles sslBundles) {
this(sslInfo, sslBundles, Clock.systemDefaultZone());
}
SslMeterBinder(SslInfo sslInfo, SslBundles sslBundles, Clock clock) {
this.clock = clock;
this.sslInfo = sslInfo;
sslBundles.addBundleRegisterHandler((bundleName, ignored) -> onBundleChange(bundleName));
for (String bundleName : sslBundles.getBundleNames()) {
sslBundles.addBundleUpdateHandler(bundleName, (ignored) -> onBundleChange(bundleName));
}
}
private void onBundleChange(String bundleName) {
BundleInfo bundle = this.sslInfo.getBundle(bundleName);
this.bundleMetrics.updateBundle(bundle);
for (MeterRegistry meterRegistry : this.bundleMetrics.getMeterRegistries()) {
createOrUpdateBundleMetrics(meterRegistry, bundle);
}
}
@Override
public void bindTo(MeterRegistry meterRegistry) {
for (BundleInfo bundle : this.sslInfo.getBundles()) {
createOrUpdateBundleMetrics(meterRegistry, bundle);
}
Gauge.builder(CHAINS_METRIC_NAME, () -> countChainsByStatus(Status.VALID))
.tag("status", "valid")
.register(meterRegistry);
Gauge.builder(CHAINS_METRIC_NAME, () -> countChainsByStatus(Status.EXPIRED))
.tag("status", "expired")
.register(meterRegistry);
Gauge.builder(CHAINS_METRIC_NAME, () -> countChainsByStatus(Status.NOT_YET_VALID))
.tag("status", "not-yet-valid")
.register(meterRegistry);
Gauge.builder(CHAINS_METRIC_NAME, () -> countChainsByStatus(Status.WILL_EXPIRE_SOON))
.tag("status", "will-expire-soon")
.register(meterRegistry);
}
private void createOrUpdateBundleMetrics(MeterRegistry meterRegistry, BundleInfo bundle) {
MultiGauge multiGauge = this.bundleMetrics.getGauge(bundle, meterRegistry);
List<Row<CertificateInfo>> rows = new ArrayList<>();
for (CertificateChainInfo chain : bundle.getCertificateChains()) {
Row<CertificateInfo> row = createRowForChain(bundle, chain);
if (row != null) {
rows.add(row);
}
}
multiGauge.register(rows, true);
}
private Row<CertificateInfo> createRowForChain(BundleInfo bundle, CertificateChainInfo chain) {
CertificateInfo leastValidCertificate = chain.getCertificates()
.stream()
.min(Comparator.comparing(CertificateInfo::getValidityEnds))
.orElse(null);
if (leastValidCertificate == null) {
return null;
}
Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), "certificate",
leastValidCertificate.getSerialNumber());
return Row.of(tags, leastValidCertificate, this::getChainExpiry);
}
private long countChainsByStatus(Status status) {
long count = 0;
for (BundleInfo bundle : this.bundleMetrics.getBundles()) {
for (CertificateChainInfo chain : bundle.getCertificateChains()) {
if (getChainStatus(chain) == status) {
count++;
}
}
}
return count;
}
private Status getChainStatus(CertificateChainInfo chain) {
EnumSet<Status> statuses = EnumSet.noneOf(Status.class);
for (CertificateInfo certificate : chain.getCertificates()) {
CertificateValidityInfo validity = certificate.getValidity();
statuses.add(validity.getStatus());
}
if (statuses.contains(Status.EXPIRED)) {
return Status.EXPIRED;
}
if (statuses.contains(Status.NOT_YET_VALID)) {
return Status.NOT_YET_VALID;
}
if (statuses.contains(Status.WILL_EXPIRE_SOON)) {
return Status.WILL_EXPIRE_SOON;
}
return statuses.isEmpty() ? null : Status.VALID;
}
private long getChainExpiry(CertificateInfo certificate) {
Duration valid = Duration.between(Instant.now(this.clock), certificate.getValidityEnds());
return valid.get(ChronoUnit.SECONDS);
}
/**
* Manages bundles and their metrics.
*/
private static final class BundleMetrics {
private final Map<String, Gauges> gauges = new ConcurrentHashMap<>();
/**
* Gets (or creates) a {@link MultiGauge} for the given bundle and meter registry.
* @param bundleInfo the bundle
* @param meterRegistry the meter registry
* @return the {@link MultiGauge}
*/
MultiGauge getGauge(BundleInfo bundleInfo, MeterRegistry meterRegistry) {
Gauges gauges = this.gauges.computeIfAbsent(bundleInfo.getName(),
(ignored) -> Gauges.emptyGauges(bundleInfo));
return gauges.getGauge(meterRegistry);
}
/**
* Returns all bundles.
* @return all bundles
*/
Collection<BundleInfo> getBundles() {
List<BundleInfo> result = new ArrayList<>();
for (Gauges metrics : this.gauges.values()) {
result.add(metrics.bundle());
}
return result;
}
/**
* Returns all meter registries.
* @return all meter registries
*/
Collection<MeterRegistry> getMeterRegistries() {
Set<MeterRegistry> result = new HashSet<>();
for (Gauges metrics : this.gauges.values()) {
result.addAll(metrics.getMeterRegistries());
}
return result;
}
/**
* Updates the given bundle.
* @param bundle the updated bundle
*/
void updateBundle(BundleInfo bundle) {
this.gauges.computeIfPresent(bundle.getName(), (key, oldValue) -> oldValue.withBundle(bundle));
}
/**
* Manages the {@link MultiGauge MultiGauges} associated to a bundle.
*
* @param bundle the bundle
* @param multiGauges mapping from meter registry to {@link MultiGauge}
*/
private record Gauges(BundleInfo bundle, Map<MeterRegistry, MultiGauge> multiGauges) {
/**
* Gets (or creates) the {@link MultiGauge} for the given meter registry.
* @param meterRegistry the meter registry
* @return the {@link MultiGauge}
*/
MultiGauge getGauge(MeterRegistry meterRegistry) {
return this.multiGauges.computeIfAbsent(meterRegistry, (ignored) -> createGauge(meterRegistry));
}
/**
* Returns a copy of this bundle with an updated {@link BundleInfo}.
* @param bundle the updated {@link BundleInfo}
* @return the copy of this bundle with an updated {@link BundleInfo}
*/
Gauges withBundle(BundleInfo bundle) {
return new Gauges(bundle, this.multiGauges);
}
/**
* Returns all meter registries.
* @return all meter registries
*/
Set<MeterRegistry> getMeterRegistries() {
return this.multiGauges.keySet();
}
private MultiGauge createGauge(MeterRegistry meterRegistry) {
return MultiGauge.builder(CHAIN_EXPIRY_METRIC_NAME)
.baseUnit("seconds")
.description("SSL chain expiry")
.register(meterRegistry);
}
/**
* Creates an instance with an empty gauge mapping.
* @param bundle the {@link BundleInfo} associated with the new instance
* @return the new instance
*/
static Gauges emptyGauges(BundleInfo bundle) {
return new Gauges(bundle, new ConcurrentHashMap<>());
}
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.ssl;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.info.SslInfo;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for SSL observability.
*
* @author Moritz Halbritter
* @since 3.5.0
*/
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SslAutoConfiguration.class })
@ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean({ MeterRegistry.class, SslBundles.class })
@EnableConfigurationProperties(SslHealthIndicatorProperties.class)
public class SslObservabilityAutoConfiguration {
@Bean
SslMeterBinder sslMeterBinder(SslInfo sslInfo, SslBundles sslBundles) {
return new SslMeterBinder(sslInfo, sslBundles);
}
@Bean
@ConditionalOnMissingBean
SslInfo sslInfoProvider(SslBundles sslBundles, SslHealthIndicatorProperties properties) {
return new SslInfo(sslBundles, properties.getCertificateValidityWarningThreshold());
}
}

View File

@ -104,6 +104,7 @@ org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSec
org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.ssl.SslHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.ssl.SslObservabilityAutoConfiguration
org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration

View File

@ -0,0 +1,107 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.ssl;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.info.SslInfo;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslMeterBinder}.
*
* @author Moritz Halbritter
*/
class SslMeterBinderTests {
private static final Clock CLOCK = Clock.fixed(Instant.parse("2024-10-21T13:51:40Z"), ZoneId.of("UTC"));
@Test
void shouldRegisterChainMetrics() {
MeterRegistry meterRegistry = bindToRegistry();
assertThat(meterRegistry.get("ssl.chains").tag("status", "valid").gauge().value()).isEqualTo(3.0);
assertThat(meterRegistry.get("ssl.chains").tag("status", "expired").gauge().value()).isEqualTo(1.0);
assertThat(meterRegistry.get("ssl.chains").tag("status", "not-yet-valid").gauge().value()).isEqualTo(1.0);
assertThat(meterRegistry.get("ssl.chains").tag("status", "will-expire-soon").gauge().value()).isEqualTo(0.0);
}
@Test
void shouldRegisterChainExpiryMetrics() {
MeterRegistry meterRegistry = bindToRegistry();
assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "ca", "419224ce190242b2c44069dd3c560192b3b669f3")))
.hasDays(1095);
assertThat(Duration
.ofSeconds(findExpiryGauge(meterRegistry, "intermediary", "60f79365fc46bf69149754d377680192b3b6bcf5")))
.hasDays(730);
assertThat(Duration
.ofSeconds(findExpiryGauge(meterRegistry, "server", "504c45129526ac050abb11459b1f0192b3b70fe9")))
.hasDays(365);
assertThat(Duration
.ofSeconds(findExpiryGauge(meterRegistry, "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc")))
.hasDays(-386);
assertThat(Duration
.ofSeconds(findExpiryGauge(meterRegistry, "not-yet-valid", "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa")))
.hasDays(36889);
}
private static long findExpiryGauge(MeterRegistry meterRegistry, String chain, String certificateSerialNumber) {
return (long) meterRegistry.get("ssl.chain.expiry")
.tag("bundle", "test-0")
.tag("chain", chain)
.tag("certificate", certificateSerialNumber)
.gauge()
.value();
}
private SimpleMeterRegistry bindToRegistry() {
SslBundles sslBundles = createSslBundles("classpath:certificates/chains.p12");
SslInfo sslInfo = createSslInfo(sslBundles);
SslMeterBinder binder = new SslMeterBinder(sslInfo, sslBundles, CLOCK);
SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
binder.bindTo(meterRegistry);
return meterRegistry;
}
private SslBundles createSslBundles(String... locations) {
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
for (int i = 0; i < locations.length; i++) {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(locations[i]).withPassword("secret");
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
sslBundleRegistry.registerBundle("test-%d".formatted(i), SslBundle.of(sslStoreBundle));
}
return sslBundleRegistry;
}
private SslInfo createSslInfo(SslBundles sslBundles) {
return new SslInfo(sslBundles, Duration.ofDays(7), CLOCK);
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.ssl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.info.SslInfo;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslObservabilityAutoConfiguration}.
*
* @author Moritz Halbritter
*/
class SslObservabilityAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
SslAutoConfiguration.class, SslObservabilityAutoConfiguration.class));
private final ApplicationContextRunner contextRunnerWithoutSslBundles = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class,
CompositeMeterRegistryAutoConfiguration.class, SslObservabilityAutoConfiguration.class));
private final ApplicationContextRunner contextRunnerWithoutMeterRegistry = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class, SslObservabilityAutoConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner
.run((context) -> assertThat(context).hasSingleBean(SslMeterBinder.class).hasSingleBean(SslInfo.class));
}
@Test
void shouldBackOffIfSslBundlesIsMissing() {
this.contextRunnerWithoutSslBundles
.run((context) -> assertThat(context).doesNotHaveBean(SslMeterBinder.class).doesNotHaveBean(SslInfo.class));
}
@Test
void shouldBackOffIfMeterRegistryIsMissing() {
this.contextRunnerWithoutMeterRegistry
.run((context) -> assertThat(context).doesNotHaveBean(SslMeterBinder.class).doesNotHaveBean(SslInfo.class));
}
}

View File

@ -865,6 +865,33 @@ To customize the tags, provide a javadoc:org.springframework.context.annotation.
[[actuator.metrics.supported.ssl]]
=== SSL bundle metrics
Spring Boot Actuator publishes two metrics about SSL bundles:
The metric `ssl.chains` gauges how many certificate chains have been registered.
The `status` tag can be used to differentiate between valid, not-yet-valid, expired and soon-to-be-expired certificates.
The metric `ssl.chain.expiry` gauges the expiry date of each certificate chain in seconds.
This number will be negative if the chain has already expired.
This metric is tagged with the following information:
|===
| Tag | Description
| `bundle`
| The name of the bundle which contains the certificate chain
| `certificate`
| The serial number (in hex format) of the certificate which is the soonest to expire in the chain
| `chain`
| The name of the certificate chain.
|===
[[actuator.metrics.supported.http-clients]]
=== HTTP Client Metrics

View File

@ -22,6 +22,7 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
@ -33,6 +34,7 @@ import java.util.function.Function;
import javax.security.auth.x500.X500Principal;
import org.springframework.boot.info.SslInfo.CertificateValidityInfo.Status;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.util.ObjectUtils;
@ -41,6 +43,7 @@ import org.springframework.util.ObjectUtils;
* Information about the certificates that the application uses.
*
* @author Jonatan Ivanov
* @author Moritz Halbritter
* @since 3.4.0
*/
public class SslInfo {
@ -49,11 +52,36 @@ public class SslInfo {
private final Duration certificateValidityWarningThreshold;
private final Clock clock;
/**
* Creates a new instance.
* @param sslBundles the {@link SslBundles} to extract the info from
* @param certificateValidityWarningThreshold the certificate validity warning
* threshold
*/
public SslInfo(SslBundles sslBundles, Duration certificateValidityWarningThreshold) {
this.sslBundles = sslBundles;
this.certificateValidityWarningThreshold = certificateValidityWarningThreshold;
this(sslBundles, certificateValidityWarningThreshold, Clock.systemDefaultZone());
}
/**
* Creates a new instance.
* @param sslBundles the {@link SslBundles} to extract the info from
* @param certificateValidityWarningThreshold the certificate validity warning
* threshold
* @param clock the {@link Clock} to use
* @since 3.5.0
*/
public SslInfo(SslBundles sslBundles, Duration certificateValidityWarningThreshold, Clock clock) {
this.sslBundles = sslBundles;
this.certificateValidityWarningThreshold = certificateValidityWarningThreshold;
this.clock = clock;
}
/**
* Returns information on all SSL bundles.
* @return information on all SSL bundles
*/
public List<BundleInfo> getBundles() {
return this.sslBundles.getBundleNames()
.stream()
@ -61,6 +89,18 @@ public class SslInfo {
.toList();
}
/**
* Returns an SSL bundle by name.
* @param name the name of the SSL bundle
* @return the {@link BundleInfo} for the given SSL bundle
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
* @since 3.5.0
*/
public BundleInfo getBundle(String name) {
SslBundle bundle = this.sslBundles.getBundle(name);
return new BundleInfo(name, bundle);
}
/**
* Info about a single {@link SslBundle}.
*/
@ -195,7 +235,7 @@ public class SslInfo {
}
private boolean isExpiringSoon(X509Certificate certificate, Duration threshold) {
Instant shouldBeValidAt = Instant.now().plus(threshold);
Instant shouldBeValidAt = Instant.now(SslInfo.this.clock).plus(threshold);
Instant expiresAt = certificate.getNotAfter().toInstant();
return shouldBeValidAt.isAfter(expiresAt);
}

View File

@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
@ -45,6 +46,8 @@ public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
private final Map<String, RegisteredSslBundle> registeredBundles = new ConcurrentHashMap<>();
private final List<BiConsumer<String, SslBundle>> registerHandlers = new CopyOnWriteArrayList<>();
public DefaultSslBundleRegistry() {
}
@ -58,6 +61,7 @@ public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
Assert.notNull(bundle, "'bundle' must not be null");
RegisteredSslBundle previous = this.registeredBundles.putIfAbsent(name, new RegisteredSslBundle(name, bundle));
Assert.state(previous == null, () -> "Cannot replace existing SSL bundle '%s'".formatted(name));
this.registerHandlers.forEach((handler) -> handler.accept(name, bundle));
}
@Override
@ -75,6 +79,11 @@ public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
getRegistered(name).addUpdateHandler(updateHandler);
}
@Override
public void addBundleRegisterHandler(BiConsumer<String, SslBundle> registerHandler) {
this.registerHandlers.add(registerHandler);
}
@Override
public List<String> getBundleNames() {
List<String> names = new ArrayList<>(this.registeredBundles.keySet());

View File

@ -17,6 +17,7 @@
package org.springframework.boot.ssl;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@ -46,6 +47,14 @@ public interface SslBundles {
*/
void addBundleUpdateHandler(String name, Consumer<SslBundle> updateHandler) throws NoSuchSslBundleException;
/**
* Add a handler that will be called each time a bundle is registered. The handler
* will be called with the bundle name and the bundle.
* @param registerHandler the handler that should be called
* @since 3.5.0
*/
void addBundleRegisterHandler(BiConsumer<String, SslBundle> registerHandler);
/**
* Return the names of all bundles managed by this instance.
* @return the bundle names