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:
parent
4685fdebf0
commit
02a49b6038
@ -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<>());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user