From d38a35b8ba11fb75bb9177fd563462e7609d9e05 Mon Sep 17 00:00:00 2001 From: bbbbooo Date: Wed, 18 Mar 2026 21:14:01 +0900 Subject: [PATCH 1/2] Monitor truststore certificates in SslMeterBinder Signed-off-by: bbbbooo --- .../reference/pages/actuator/metrics.adoc | 5 +- .../autoconfigure/ssl/SslMeterBinder.java | 21 +++-- .../ssl/SslMeterBinderTests.java | 88 ++++++++++++++++++- 3 files changed, 106 insertions(+), 8 deletions(-) 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 563acd583377..c5ee2848e677 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. + +| `store` +| Whether the certificate chain comes from the key store (`key`) or trust store (`trust`) |=== 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 db3565bd17d4..6c4bcde6b235 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,10 @@ class SslMeterBinder implements MeterBinder { private static final String CHAIN_EXPIRY_METRIC_NAME = "ssl.chain.expiry"; + private static final String KEY_STORE_TAG_VALUE = "key"; + + private static final String TRUST_STORE_TAG_VALUE = "trust"; + private final Clock clock; private final SslInfo sslInfo; @@ -95,16 +99,23 @@ public void bindTo(MeterRegistry 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); + addRows(rows, bundle, bundle.getCertificateChains(), KEY_STORE_TAG_VALUE); + addRows(rows, bundle, bundle.getTrustStoreCertificateChains(), TRUST_STORE_TAG_VALUE); + multiGauge.register(rows, true); + } + + private void addRows(List> rows, BundleInfo bundle, List chains, + String store) { + for (CertificateChainInfo chain : chains) { + Row row = createRowForChain(bundle, chain, store); 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 store) { CertificateInfo leastValidCertificate = chain.getCertificates() .stream() .filter((c) -> c.getValidityEnds() != null) @@ -114,7 +125,7 @@ private void createOrUpdateBundleMetrics(MeterRegistry meterRegistry, BundleInfo return null; } String serialNumber = leastValidCertificate.getSerialNumber(); - Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), "certificate", + Tags tags = Tags.of("chain", chain.getAlias(), "bundle", bundle.getName(), "store", store, "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 a85e1f5bad01..77b32bbe0194 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 @@ void shouldRegisterChainExpiryMetrics() { .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, "trust", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "trust", "intermediary", "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "trust", "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, "key", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + } + @Test void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -90,6 +128,35 @@ void shouldWatchUpdatesForBundlesRegisteredAfterConstruction() { .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", "store", "trust").meters()) + .hasSize(5); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "ca", "419224ce190242b2c44069dd3c560192b3b669f3"))) + .hasDays(1095); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "trust", "intermediary", "60f79365fc46bf69149754d377680192b3b6bcf5"))) + .hasDays(730); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "server", "504c45129526ac050abb11459b1f0192b3b70fe9"))) + .hasDays(365); + assertThat(Duration + .ofSeconds(findExpiryGauge(meterRegistry, "trust", "expired", "562bc5dcf4f26bb179abb13068180192b3bb53dc"))) + .hasDays(-386); + assertThat(Duration.ofSeconds( + findExpiryGauge(meterRegistry, "trust", "not-yet-valid", "7df79335f274e2cfa7467fd5f9ce0192b3bcf4aa"))) + .hasDays(36889); + } + @Test void shouldRegisterMetricsIfNoBundleExistsAtBindTime() { DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry(); @@ -100,8 +167,14 @@ void shouldRegisterMetricsIfNoBundleExistsAtBindTime() { } private long findExpiryGauge(MeterRegistry meterRegistry, String chain, String certificateSerialNumber) { + return findExpiryGauge(meterRegistry, "key", chain, certificateSerialNumber); + } + + private long findExpiryGauge(MeterRegistry meterRegistry, String store, String chain, + String certificateSerialNumber) { return (long) meterRegistry.get("ssl.chain.expiry") .tag("bundle", "test-0") + .tag("store", store) .tag("chain", chain) .tag("certificate", certificateSerialNumber) .gauge() @@ -117,8 +190,19 @@ private SimpleMeterRegistry bindToRegistry(SslBundles sslBundles) { } 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) { From b13025c28199ee680c15bf631cddb116325c7e17 Mon Sep 17 00:00:00 2001 From: bbbbooo Date: Thu, 19 Mar 2026 21:02:06 +0900 Subject: [PATCH 2/2] Rename SSL chain expiry tag from store to source Signed-off-by: bbbbooo --- .../reference/pages/actuator/metrics.adoc | 4 +- .../autoconfigure/ssl/SslMeterBinder.java | 20 ++++---- .../ssl/SslMeterBinderTests.java | 48 +++++++++---------- 3 files changed, 37 insertions(+), 35 deletions(-) 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 c5ee2848e677..19e4c024ac51 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 @@ -831,8 +831,8 @@ This metric is tagged with the following information: | `chain` | The name of the certificate chain. -| `store` -| Whether the certificate chain comes from the key store (`key`) or trust store (`trust`) +| `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 6c4bcde6b235..9474cc8f5a0f 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,9 +52,11 @@ class SslMeterBinder implements MeterBinder { private static final String CHAIN_EXPIRY_METRIC_NAME = "ssl.chain.expiry"; - private static final String KEY_STORE_TAG_VALUE = "key"; + private static final String SOURCE_TAG_NAME = "source"; - private static final String TRUST_STORE_TAG_VALUE = "trust"; + private static final String KEY_STORE_SOURCE_TAG_VALUE = "keystore"; + + private static final String TRUST_STORE_SOURCE_TAG_VALUE = "truststore"; private final Clock clock; @@ -99,15 +101,15 @@ public void bindTo(MeterRegistry meterRegistry) { private void createOrUpdateBundleMetrics(MeterRegistry meterRegistry, BundleInfo bundle) { MultiGauge multiGauge = this.bundleMetrics.getGauge(bundle, meterRegistry); List> rows = new ArrayList<>(); - addRows(rows, bundle, bundle.getCertificateChains(), KEY_STORE_TAG_VALUE); - addRows(rows, bundle, bundle.getTrustStoreCertificateChains(), TRUST_STORE_TAG_VALUE); + 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 store) { + String source) { for (CertificateChainInfo chain : chains) { - Row row = createRowForChain(bundle, chain, store); + Row row = createRowForChain(bundle, chain, source); if (row != null) { rows.add(row); } @@ -115,7 +117,7 @@ private void addRows(List> rows, BundleInfo bundle, List createRowForChain(BundleInfo bundle, CertificateChainInfo chain, - String store) { + String source) { CertificateInfo leastValidCertificate = chain.getCertificates() .stream() .filter((c) -> c.getValidityEnds() != null) @@ -125,8 +127,8 @@ private void addRows(List> rows, BundleInfo bundle, List