Monitor truststore certificates in SslMeterBinder

See gh-49641

Signed-off-by: bbbbooo <hyeons1213@gmail.com>
This commit is contained in:
bbbbooo
2026-04-21 12:25:49 +02:00
committed by Moritz Halbritter
parent c06489019b
commit f169bf8a48
3 changed files with 109 additions and 9 deletions
@@ -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`)
|===
@@ -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<Row<CertificateInfo>> rows = new ArrayList<>();
for (CertificateChainInfo chain : bundle.getCertificateChains()) {
Row<CertificateInfo> 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<Row<CertificateInfo>> rows, BundleInfo bundle, List<CertificateChainInfo> chains,
String source) {
for (CertificateChainInfo chain : chains) {
Row<CertificateInfo> row = createRowForChain(bundle, chain, source);
if (row != null) {
rows.add(row);
}
}
multiGauge.register(rows, true);
}
private @Nullable Row<CertificateInfo> createRowForChain(BundleInfo bundle, CertificateChainInfo chain) {
private @Nullable Row<CertificateInfo> 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);
}
@@ -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) {