From 02a49b6038b93688c113c14518ec79931073bf4d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 21 Oct 2024 15:01:27 +0200 Subject: [PATCH] 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 --- .../autoconfigure/ssl/SslMeterBinder.java | 272 ++++++++++++++++++ .../SslObservabilityAutoConfiguration.java | 58 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ssl/SslMeterBinderTests.java | 107 +++++++ ...slObservabilityAutoConfigurationTests.java | 66 +++++ .../test/resources/certificates/chains.p12 | Bin 0 -> 13800 bytes .../reference/pages/actuator/metrics.adoc | 27 ++ .../springframework/boot/info/SslInfo.java | 46 ++- .../boot/ssl/DefaultSslBundleRegistry.java | 9 + .../springframework/boot/ssl/SslBundles.java | 9 + 10 files changed, 592 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinder.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinderTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/certificates/chains.p12 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinder.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinder.java new file mode 100644 index 00000000000..9e9c93a2cf6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinder.java @@ -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> rows = new ArrayList<>(); + for (CertificateChainInfo chain : bundle.getCertificateChains()) { + Row row = createRowForChain(bundle, chain); + if (row != null) { + rows.add(row); + } + } + multiGauge.register(rows, true); + } + + private Row 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 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 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 getBundles() { + List result = new ArrayList<>(); + for (Gauges metrics : this.gauges.values()) { + result.add(metrics.bundle()); + } + return result; + } + + /** + * Returns all meter registries. + * @return all meter registries + */ + Collection getMeterRegistries() { + Set 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 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 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<>()); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfiguration.java new file mode 100644 index 00000000000..a763dd0a15c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfiguration.java @@ -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()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c5416d52544..201b9cfdaed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinderTests.java new file mode 100644 index 00000000000..7f630c03d84 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslMeterBinderTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfigurationTests.java new file mode 100644 index 00000000000..39cb14182d1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslObservabilityAutoConfigurationTests.java @@ -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)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/certificates/chains.p12 b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/certificates/chains.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b0a8d29a2b7594a095484acd72abd91eda366581 GIT binary patch literal 13800 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 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); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java index 0cbe28f0d3e..55f2a66addb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java @@ -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 registeredBundles = new ConcurrentHashMap<>(); + private final List> 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 registerHandler) { + this.registerHandlers.add(registerHandler); + } + @Override public List getBundleNames() { List names = new ArrayList<>(this.registeredBundles.keySet()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java index 91bce68fdc0..10fe2f9130a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java @@ -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 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 registerHandler); + /** * Return the names of all bundles managed by this instance. * @return the bundle names