Skip to content

Commit fd14727

Browse files
committed
Polish "Add SslInfoContributor and SslHealthIndicator"
See gh-41205
1 parent 5e3796e commit fd14727

File tree

11 files changed

+109
-103
lines changed

11 files changed

+109
-103
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ public ProcessInfoContributor processInfoContributor() {
107107
@Bean
108108
@ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE)
109109
@Order(DEFAULT_ORDER)
110-
public SslInfoContributor sslInfoContributor(SslInfo sslInfo) {
110+
SslInfoContributor sslInfoContributor(SslInfo sslInfo) {
111111
return new SslInfoContributor(sslInfo);
112112
}
113113

114114
@Bean
115115
@ConditionalOnMissingBean
116116
@ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE)
117-
public SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
117+
SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
118118
return new SslInfo(sslBundles, sslHealthIndicatorProperties.getCertificateValidityWarningThreshold());
119119
}
120120

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ public class SslHealthContributorAutoConfiguration {
4040

4141
@Bean
4242
@ConditionalOnMissingBean(name = "sslHealthIndicator")
43-
public SslHealthIndicator sslHealthIndicator(SslInfo sslInfo) {
43+
SslHealthIndicator sslHealthIndicator(SslInfo sslInfo) {
4444
return new SslHealthIndicator(sslInfo);
4545
}
4646

4747
@Bean
4848
@ConditionalOnMissingBean
49-
public SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
49+
SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
5050
return new SslInfo(sslBundles, sslHealthIndicatorProperties.getCertificateValidityWarningThreshold());
5151
}
5252

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,6 @@
227227
"description": "Whether to enable Redis health check.",
228228
"defaultValue": true
229229
},
230-
{
231-
"name": "management.health.ssl.certificate-validity-warning-threshold",
232-
"type": "java.time.Duration",
233-
"description": "If an SSL Certificate will be invalid within the time span defined by this threshold, it should trigger a warning.",
234-
"defaultValue": "14d"
235-
},
236230
{
237231
"name": "management.health.ssl.enabled",
238232
"type": "java.lang.Boolean",

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfigurationTests.java

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939

4040
/**
4141
* Tests for {@link SslHealthContributorAutoConfiguration}.
42+
*
43+
* @author Jonatan Ivanov
4244
*/
4345
class SslHealthContributorAutoConfigurationTests {
4446

@@ -56,24 +58,21 @@ void beansShouldNotBeConfigured() {
5658
}
5759

5860
@Test
59-
@SuppressWarnings("unchecked")
6061
void beansShouldBeConfigured() {
6162
this.contextRunner.run((context) -> {
6263
assertThat(context).hasSingleBean(SslHealthIndicator.class);
6364
assertThat(context).hasSingleBean(SslInfo.class);
6465
Health health = context.getBean(SslHealthIndicator.class).health();
6566
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
66-
assertThat(health.getDetails()).hasSize(1);
67-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
68-
.get("certificateChains");
69-
assertThat(certificateChains).hasSize(1);
70-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
67+
assertDetailsKeys(health);
68+
List<CertificateChain> invalidChains = getInvalidChains(health);
69+
assertThat(invalidChains).hasSize(1);
70+
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
7171

7272
});
7373
}
7474

7575
@Test
76-
@SuppressWarnings("unchecked")
7776
void beansShouldBeConfiguredWithWarningThreshold() {
7877
this.contextRunner.withPropertyValues("management.health.ssl.certificate-validity-warning-threshold=1d")
7978
.run((context) -> {
@@ -84,16 +83,14 @@ void beansShouldBeConfiguredWithWarningThreshold() {
8483
.isEqualTo(Duration.ofDays(1));
8584
Health health = context.getBean(SslHealthIndicator.class).health();
8685
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
87-
assertThat(health.getDetails()).hasSize(1);
88-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
89-
.get("certificateChains");
90-
assertThat(certificateChains).hasSize(1);
91-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
86+
assertDetailsKeys(health);
87+
List<CertificateChain> invalidChains = getInvalidChains(health);
88+
assertThat(invalidChains).hasSize(1);
89+
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
9290
});
9391
}
9492

9593
@Test
96-
@SuppressWarnings("unchecked")
9794
void customBeansShouldBeConfigured() {
9895
this.contextRunner.withUserConfiguration(CustomSslInfoConfiguration.class).run((context) -> {
9996
assertThat(context).hasSingleBean(SslHealthIndicator.class);
@@ -103,14 +100,22 @@ void customBeansShouldBeConfigured() {
103100
assertThat(context.getBean(SslInfo.class)).isSameAs(context.getBean("customSslInfo"));
104101
Health health = context.getBean(SslHealthIndicator.class).health();
105102
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
106-
assertThat(health.getDetails()).hasSize(1);
107-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
108-
.get("certificateChains");
109-
assertThat(certificateChains).hasSize(1);
110-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
103+
assertDetailsKeys(health);
104+
List<CertificateChain> invalidChains = getInvalidChains(health);
105+
assertThat(invalidChains).hasSize(1);
106+
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
111107
});
112108
}
113109

110+
private static void assertDetailsKeys(Health health) {
111+
assertThat(health.getDetails()).containsOnlyKeys("validChains", "invalidChains");
112+
}
113+
114+
@SuppressWarnings("unchecked")
115+
private static List<CertificateChain> getInvalidChains(Health health) {
116+
return (List<CertificateChain>) health.getDetails().get("invalidChains");
117+
}
118+
114119
@Configuration(proxyBeanMethods = false)
115120
static class CustomSslInfoConfiguration {
116121

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,17 @@
1717
package org.springframework.boot.actuate.ssl;
1818

1919
import java.util.List;
20-
import java.util.Set;
21-
import java.util.stream.Collectors;
2220

2321
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
2422
import org.springframework.boot.actuate.health.Health.Builder;
2523
import org.springframework.boot.actuate.health.HealthIndicator;
2624
import org.springframework.boot.actuate.health.Status;
2725
import org.springframework.boot.info.SslInfo;
2826
import org.springframework.boot.info.SslInfo.CertificateChain;
29-
import org.springframework.boot.info.SslInfo.CertificateInfo.Validity;
3027

3128
/**
3229
* {@link HealthIndicator} that checks the certificates the application uses and reports
33-
* {@link Status#OUT_OF_SERVICE} when a certificate is invalid or "WILL_EXPIRE_SOON" if it
34-
* will expire within the configurable threshold.
30+
* {@link Status#OUT_OF_SERVICE} when a certificate is invalid.
3531
*
3632
* @author Jonatan Ivanov
3733
* @since 3.4.0
@@ -46,43 +42,38 @@ public SslHealthIndicator(SslInfo sslInfo) {
4642

4743
@Override
4844
protected void doHealthCheck(Builder builder) throws Exception {
49-
List<CertificateChain> notValidCertificateChains = this.sslInfo.getBundles()
45+
List<CertificateChain> certificateChains = this.sslInfo.getBundles()
5046
.stream()
5147
.flatMap((bundle) -> bundle.getCertificateChains().stream())
52-
.filter(this::containsNotValidCertificate)
5348
.toList();
54-
55-
if (notValidCertificateChains.isEmpty()) {
49+
List<CertificateChain> validCertificateChains = certificateChains.stream()
50+
.filter(this::containsOnlyValidCertificates)
51+
.toList();
52+
List<CertificateChain> invalidCertificateChains = certificateChains.stream()
53+
.filter(this::containsInvalidCertificate)
54+
.toList();
55+
builder.withDetail("validChains", validCertificateChains);
56+
builder.withDetail("invalidChains", invalidCertificateChains);
57+
if (invalidCertificateChains.isEmpty()) {
5658
builder.status(Status.UP);
5759
}
5860
else {
59-
Set<Validity.Status> statuses = collectCertificateStatuses(notValidCertificateChains);
60-
if (statuses.contains(Validity.Status.EXPIRED) || statuses.contains(Validity.Status.NOT_YET_VALID)) {
61-
builder.status(Status.OUT_OF_SERVICE);
62-
}
63-
else if (statuses.contains(Validity.Status.WILL_EXPIRE_SOON)) {
64-
builder.status(Status.UP);
65-
}
66-
else {
67-
builder.status(Status.OUT_OF_SERVICE);
68-
}
69-
builder.withDetail("certificateChains", notValidCertificateChains);
61+
builder.status(Status.OUT_OF_SERVICE);
7062
}
7163
}
7264

73-
private boolean containsNotValidCertificate(CertificateChain certificateChain) {
65+
private boolean containsOnlyValidCertificates(CertificateChain certificateChain) {
7466
return certificateChain.getCertificates()
7567
.stream()
7668
.filter((certificate) -> certificate.getValidity() != null)
77-
.anyMatch((certificate) -> certificate.getValidity().getStatus() != Validity.Status.VALID);
69+
.allMatch((certificate) -> certificate.getValidity().getStatus().isValid());
7870
}
7971

80-
private Set<Validity.Status> collectCertificateStatuses(List<CertificateChain> certificateChains) {
81-
return certificateChains.stream()
82-
.flatMap((certificateChain) -> certificateChain.getCertificates().stream())
72+
private boolean containsInvalidCertificate(CertificateChain certificateChain) {
73+
return certificateChain.getCertificates()
74+
.stream()
8375
.filter((certificate) -> certificate.getValidity() != null)
84-
.map((certificate) -> certificate.getValidity().getStatus())
85-
.collect(Collectors.toUnmodifiableSet());
76+
.anyMatch((certificate) -> !certificate.getValidity().getStatus().isValid());
8677
}
8778

8879
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/ssl/SslHealthIndicatorTests.java

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,59 +66,66 @@ void shouldBeUpIfNoSslIssuesDetected() {
6666
given(this.validity.getStatus()).willReturn(Validity.Status.VALID);
6767
Health health = this.healthIndicator.health();
6868
assertThat(health.getStatus()).isEqualTo(Status.UP);
69-
assertThat(health.getDetails()).isEmpty();
69+
assertDetailsKeys(health);
70+
List<CertificateChain> validChains = getValidChains(health);
71+
assertThat(validChains).hasSize(1);
72+
assertThat(validChains.get(0)).isInstanceOf(CertificateChain.class);
73+
List<CertificateChain> invalidChains = getInvalidChains(health);
74+
assertThat(invalidChains).isEmpty();
7075
}
7176

7277
@Test
73-
@SuppressWarnings("unchecked")
7478
void shouldBeOutOfServiceIfACertificateIsExpired() {
7579
given(this.validity.getStatus()).willReturn(Validity.Status.EXPIRED);
7680
Health health = this.healthIndicator.health();
7781
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
78-
assertThat(health.getDetails()).hasSize(1);
79-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
80-
.get("certificateChains");
81-
assertThat(certificateChains).hasSize(1);
82-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
82+
assertDetailsKeys(health);
83+
List<CertificateChain> validChains = getValidChains(health);
84+
assertThat(validChains).isEmpty();
85+
List<CertificateChain> invalidChains = getInvalidChains(health);
86+
assertThat(invalidChains).hasSize(1);
87+
assertThat(invalidChains.get(0)).isInstanceOf(CertificateChain.class);
8388
}
8489

8590
@Test
86-
@SuppressWarnings("unchecked")
8791
void shouldBeOutOfServiceIfACertificateIsNotYetValid() {
8892
given(this.validity.getStatus()).willReturn(Validity.Status.NOT_YET_VALID);
8993
Health health = this.healthIndicator.health();
9094
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
91-
assertThat(health.getDetails()).hasSize(1);
92-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
93-
.get("certificateChains");
94-
assertThat(certificateChains).hasSize(1);
95-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
95+
assertDetailsKeys(health);
96+
List<CertificateChain> validChains = getValidChains(health);
97+
assertThat(validChains).isEmpty();
98+
List<CertificateChain> invalidChains = getInvalidChains(health);
99+
assertThat(invalidChains).hasSize(1);
100+
assertThat(invalidChains.get(0)).isInstanceOf(CertificateChain.class);
101+
96102
}
97103

98104
@Test
99-
@SuppressWarnings("unchecked")
100105
void shouldReportWarningIfACertificateWillExpireSoon() {
101106
given(this.validity.getStatus()).willReturn(Validity.Status.WILL_EXPIRE_SOON);
102107
Health health = this.healthIndicator.health();
103108
assertThat(health.getStatus()).isEqualTo(Status.UP);
104-
assertThat(health.getDetails()).hasSize(1);
105-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
106-
.get("certificateChains");
107-
assertThat(certificateChains).hasSize(1);
108-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
109+
assertDetailsKeys(health);
110+
List<CertificateChain> validChains = getValidChains(health);
111+
assertThat(validChains).hasSize(1);
112+
assertThat(validChains.get(0)).isInstanceOf(CertificateChain.class);
113+
List<CertificateChain> invalidChains = getInvalidChains(health);
114+
assertThat(invalidChains).isEmpty();
115+
}
116+
117+
private static void assertDetailsKeys(Health health) {
118+
assertThat(health.getDetails()).containsOnlyKeys("validChains", "invalidChains");
109119
}
110120

111-
@Test
112121
@SuppressWarnings("unchecked")
113-
void shouldBeOutOfServiceIfACertificateHasUnMappedValidityStatus() {
114-
given(this.validity.getStatus()).willReturn(mock(Validity.Status.class));
115-
Health health = this.healthIndicator.health();
116-
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
117-
assertThat(health.getDetails()).hasSize(1);
118-
List<CertificateChain> certificateChains = (List<CertificateChain>) health.getDetails()
119-
.get("certificateChains");
120-
assertThat(certificateChains).hasSize(1);
121-
assertThat(certificateChains.get(0)).isInstanceOf(CertificateChain.class);
122+
private static List<CertificateChain> getInvalidChains(Health health) {
123+
return (List<CertificateChain>) health.getDetails().get("invalidChains");
124+
}
125+
126+
@SuppressWarnings("unchecked")
127+
private static List<CertificateChain> getValidChains(Health health) {
128+
return (List<CertificateChain>) health.getDetails().get("validChains");
122129
}
123130

124131
}

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/endpoints.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,12 +651,14 @@ with the `key` listed in the following table:
651651

652652
| `ssl`
653653
| javadoc:org.springframework.boot.actuate.ssl.SslHealthIndicator[]
654-
| Checks that SSL Cerificates are ok.
654+
| Checks that SSL Certificates are ok.
655655
|===
656656

657657
TIP: You can disable them all by setting the configprop:management.health.defaults.enabled[] property.
658658

659-
TIP: The `ssl` `HealthIndicator` has a "warning threshold" property. If an SSL Certificate will be invalid within the time span defined by this threshold, the `HealthIndicator` will warn you but it will still return HTTP 200 to not disrupt the application. You can use this threshold to give yourself enough lead time to rotate the soon to be expired certificate. See the `management.health.ssl.certificate-validity-warning-threshold` property.
659+
TIP: The `ssl` `HealthIndicator` has a "warning threshold" property named configprop:management.health.ssl.certificate-validity-warning-threshold[].
660+
If an SSL Certificate will be invalid within the time span defined by this threshold, the `HealthIndicator` will warn you but it will still return HTTP 200 to not disrupt the application.
661+
You can use this threshold to give yourself enough lead time to rotate the soon to be expired certificate.
660662

661663

662664

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,23 +216,33 @@ public enum Status {
216216
/**
217217
* The certificate is valid.
218218
*/
219-
VALID,
219+
VALID(true),
220220

221221
/**
222222
* The certificate's validity date range is in the future.
223223
*/
224-
NOT_YET_VALID,
224+
NOT_YET_VALID(false),
225225

226226
/**
227227
* The certificate's validity date range is in the past.
228228
*/
229-
EXPIRED,
229+
EXPIRED(false),
230230

231231
/**
232-
* The certificate is still valid but the end of its validity date range
232+
* The certificate is still valid, but the end of its validity date range
233233
* is within the defined threshold.
234234
*/
235-
WILL_EXPIRE_SOON
235+
WILL_EXPIRE_SOON(true);
236+
237+
private final boolean valid;
238+
239+
Status(boolean valid) {
240+
this.valid = valid;
241+
}
242+
243+
public boolean isValid() {
244+
return this.valid;
245+
}
236246

237247
}
238248

0 commit comments

Comments
 (0)