diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index 0cec2d64890..9b20630a8c6 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -815,7 +815,7 @@ To customize the tags, provide a javadoc:org.springframework.context.annotation. === SSL Bundle Metrics Spring Boot Actuator publishes expiry metrics about SSL bundles. -The metric `ssl.chain.expiry` gauges the expiry date of each certificate chain in seconds. +The metric `ssl.chain.expiry` gauges the expiry date of each certificate chain in key stores and trust stores in seconds. This number will be negative if the chain has already expired. This metric is tagged with the following information: @@ -830,6 +830,9 @@ This metric is tagged with the following information: | `chain` | The name of the certificate chain. + +| `source` +| Whether the certificate chain comes from the key store (`keystore`) or trust store (`truststore`) |=== diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java index db3565bd17d..9474cc8f5a0 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinder.java @@ -52,6 +52,12 @@ class SslMeterBinder implements MeterBinder { private static final String CHAIN_EXPIRY_METRIC_NAME = "ssl.chain.expiry"; + private static final String SOURCE_TAG_NAME = "source"; + + private static final String KEY_STORE_SOURCE_TAG_VALUE = "keystore"; + + private static final String TRUST_STORE_SOURCE_TAG_VALUE = "truststore"; + private final Clock clock; private final SslInfo sslInfo; @@ -95,16 +101,23 @@ class SslMeterBinder implements MeterBinder { 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); + addRows(rows, bundle, bundle.getCertificateChains(), KEY_STORE_SOURCE_TAG_VALUE); + addRows(rows, bundle, bundle.getTrustStoreCertificateChains(), TRUST_STORE_SOURCE_TAG_VALUE); + multiGauge.register(rows, true); + } + + private void addRows(List> rows, BundleInfo bundle, List chains, + String source) { + for (CertificateChainInfo chain : chains) { + Row row = createRowForChain(bundle, chain, source); if (row != null) { rows.add(row); } } - multiGauge.register(rows, true); } - private @Nullable Row createRowForChain(BundleInfo bundle, CertificateChainInfo chain) { + private @Nullable Row createRowForChain(BundleInfo bundle, CertificateChainInfo chain, + String source) { CertificateInfo leastValidCertificate = chain.getCertificates() .stream() .filter((c) -> c.getValidityEnds() != null) @@ -114,8 +127,8 @@ class SslMeterBinder implements MeterBinder { return null; } String serialNumber = leastValidCertificate.getSerialNumber(); - Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), "certificate", - (serialNumber != null) ? serialNumber : ""); + Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), SOURCE_TAG_NAME, source, + "certificate", (serialNumber != null) ? serialNumber : ""); return Row.of(tags, leastValidCertificate, this::getChainExpiry); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java index a85e1f5bad0..e8c8151ea57 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/ssl/SslMeterBinderTests.java @@ -64,6 +64,44 @@ class SslMeterBinderTests { .hasDays(36889); } + @Test + void shouldRegisterTrustStoreChainExpiryMetrics() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "intermediary", + "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "not-yet-valid", + "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa"))) + .hasDays(36889); + } + + @Test + void shouldDifferentiateKeyStoreAndTrustStoreMetrics() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createKeyAndTrustStoreBundle("classpath:certificates/chains.p12", + "classpath:certificates/chains.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "keystore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + } + @Test void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -90,6 +128,35 @@ class SslMeterBinderTests { .hasDays(36889); } + @Test + void shouldWatchTrustStoreUpdatesForBundlesRegisteredAfterConstruction() { + DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); + sslBundleRegistry.registerBundle("dummy", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains2.p12"))); + MeterRegistry meterRegistry = bindToRegistry(sslBundleRegistry); + sslBundleRegistry.registerBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains2.p12"))); + sslBundleRegistry.updateBundle("test-0", + SslBundle.of(createTrustStoreBundle("classpath:certificates/chains.p12"))); + assertThat(meterRegistry.find("ssl.chain.expiry").tags("bundle", "test-0", "source", "truststore").meters()) + .hasSize(5); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "truststore", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "intermediary", + "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "truststore", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds(findExpiryGauge(meterRegistry, "truststore", "not-yet-valid", + "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa"))) + .hasDays(36889); + } + @Test void shouldRegisterMetricsIfNoBundleExistsAtBindTime() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -100,8 +167,14 @@ class SslMeterBinderTests { } private long findExpiryGauge(MeterRegistry meterRegistry, String chain, String certificateSerialNumber) { + return findExpiryGauge(meterRegistry, "keystore", chain, certificateSerialNumber); + } + + private long findExpiryGauge(MeterRegistry meterRegistry, String source, String chain, + String certificateSerialNumber) { return (long) meterRegistry.get("ssl.chain.expiry") .tag("bundle", "test-0") + .tag("source", source) .tag("chain", chain) .tag("certificate", certificateSerialNumber) .gauge() @@ -117,8 +190,19 @@ class SslMeterBinderTests { } private SslStoreBundle createSslStoreBundle(String location) { - JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(location).withPassword("secret"); - return new JksSslStoreBundle(keyStoreDetails, null); + return new JksSslStoreBundle(createStoreDetails(location), null); + } + + private SslStoreBundle createTrustStoreBundle(String location) { + return new JksSslStoreBundle(null, createStoreDetails(location)); + } + + private SslStoreBundle createKeyAndTrustStoreBundle(String keyStoreLocation, String trustStoreLocation) { + return new JksSslStoreBundle(createStoreDetails(keyStoreLocation), createStoreDetails(trustStoreLocation)); + } + + private JksSslStoreDetails createStoreDetails(String location) { + return JksSslStoreDetails.forLocation(location).withPassword("secret"); } private DefaultSslBundleRegistry createSslBundleRegistry(String... locations) {