Merge pull request #41205 from jonatan-ivanov
* pr/41205: Polish "Add SslInfoContributor and SslHealthIndicator" Add SslInfoContributor and SslHealthIndicator Closes gh-41205
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.actuate.autoconfigure.info;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.ssl.SslHealthIndicatorProperties;
|
||||
import org.springframework.boot.actuate.info.BuildInfoContributor;
|
||||
import org.springframework.boot.actuate.info.EnvironmentInfoContributor;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
@@ -23,6 +24,7 @@ import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.actuate.info.JavaInfoContributor;
|
||||
import org.springframework.boot.actuate.info.OsInfoContributor;
|
||||
import org.springframework.boot.actuate.info.ProcessInfoContributor;
|
||||
import org.springframework.boot.actuate.info.SslInfoContributor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -31,6 +33,8 @@ import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.boot.info.GitProperties;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -46,7 +50,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = ProjectInfoAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(InfoContributorProperties.class)
|
||||
@EnableConfigurationProperties({ InfoContributorProperties.class, SslHealthIndicatorProperties.class })
|
||||
public class InfoContributorAutoConfiguration {
|
||||
|
||||
/**
|
||||
@@ -100,4 +104,18 @@ public class InfoContributorAutoConfiguration {
|
||||
return new ProcessInfoContributor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE)
|
||||
@Order(DEFAULT_ORDER)
|
||||
SslInfoContributor sslInfoContributor(SslInfo sslInfo) {
|
||||
return new SslInfoContributor(sslInfo);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE)
|
||||
SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
|
||||
return new SslInfo(sslBundles, sslHealthIndicatorProperties.getCertificateValidityWarningThreshold());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
|
||||
import org.springframework.boot.actuate.ssl.SslHealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
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 {@link SslHealthIndicator}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@AutoConfiguration(before = HealthContributorAutoConfiguration.class)
|
||||
@ConditionalOnEnabledHealthIndicator("ssl")
|
||||
@EnableConfigurationProperties(SslHealthIndicatorProperties.class)
|
||||
public class SslHealthContributorAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "sslHealthIndicator")
|
||||
SslHealthIndicator sslHealthIndicator(SslInfo sslInfo) {
|
||||
return new SslHealthIndicator(sslInfo);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SslInfo sslInfo(SslBundles sslBundles, SslHealthIndicatorProperties sslHealthIndicatorProperties) {
|
||||
return new SslInfo(sslBundles, sslHealthIndicatorProperties.getCertificateValidityWarningThreshold());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.Duration;
|
||||
|
||||
import org.springframework.boot.actuate.ssl.SslHealthIndicator;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* External configuration properties for {@link SslHealthIndicator}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "management.health.ssl")
|
||||
public class SslHealthIndicatorProperties {
|
||||
|
||||
/**
|
||||
* If an SSL Certificate will be invalid within the time span defined by this
|
||||
* threshold, it should trigger a warning.
|
||||
*/
|
||||
private Duration certificateValidityWarningThreshold = Duration.ofDays(14);
|
||||
|
||||
public Duration getCertificateValidityWarningThreshold() {
|
||||
return this.certificateValidityWarningThreshold;
|
||||
}
|
||||
|
||||
public void setCertificateValidityWarningThreshold(Duration certificateValidityWarningThreshold) {
|
||||
this.certificateValidityWarningThreshold = certificateValidityWarningThreshold;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for actuator ssl concerns.
|
||||
*/
|
||||
package org.springframework.boot.actuate.autoconfigure.ssl;
|
||||
@@ -227,6 +227,12 @@
|
||||
"description": "Whether to enable Redis health check.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "management.health.ssl.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable SSL Certificate health check.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "management.httpexchanges.recording.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
@@ -283,6 +289,12 @@
|
||||
"description": "Whether to enable process info.",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "management.info.ssl.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable SSL Certificate info.",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "management.metrics.binders.files.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
|
||||
@@ -104,6 +104,7 @@ org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagem
|
||||
org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
|
||||
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.system.DiskSpaceHealthContributorAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
package org.springframework.boot.actuate.autoconfigure.info;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.ssl.SslHealthIndicatorProperties;
|
||||
import org.springframework.boot.actuate.info.BuildInfoContributor;
|
||||
import org.springframework.boot.actuate.info.EnvironmentInfoContributor;
|
||||
import org.springframework.boot.actuate.info.GitInfoContributor;
|
||||
@@ -29,12 +31,16 @@ import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.actuate.info.JavaInfoContributor;
|
||||
import org.springframework.boot.actuate.info.OsInfoContributor;
|
||||
import org.springframework.boot.actuate.info.ProcessInfoContributor;
|
||||
import org.springframework.boot.actuate.info.SslInfoContributor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.boot.info.GitProperties;
|
||||
import org.springframework.boot.info.JavaInfo;
|
||||
import org.springframework.boot.info.OsInfo;
|
||||
import org.springframework.boot.info.ProcessInfo;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -60,7 +66,8 @@ class InfoContributorAutoConfigurationTests {
|
||||
|
||||
@Test
|
||||
void defaultInfoContributorsEnabled() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfoContributor.class));
|
||||
this.contextRunner.run(
|
||||
(context) -> assertThat(context).doesNotHaveBean(InfoContributor.class).doesNotHaveBean(SslInfo.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -176,6 +183,54 @@ class InfoContributorAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslInfoContributor() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class))
|
||||
.withPropertyValues("management.info.ssl.enabled=true", "server.ssl.bundle=ssltest",
|
||||
"spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslInfoContributor.class);
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
Map<String, Object> content = invokeContributor(context.getBean(SslInfoContributor.class));
|
||||
assertThat(content).containsKey("ssl");
|
||||
assertThat(content.get("ssl")).isInstanceOf(SslInfo.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslInfoContributorWithWarningThreshold() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class))
|
||||
.withPropertyValues("management.info.ssl.enabled=true", "server.ssl.bundle=ssltest",
|
||||
"spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks",
|
||||
"management.health.ssl.certificate-validity-warning-threshold=1d")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslInfoContributor.class);
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
assertThat(context).hasSingleBean(SslHealthIndicatorProperties.class);
|
||||
assertThat(context.getBean(SslHealthIndicatorProperties.class).getCertificateValidityWarningThreshold())
|
||||
.isEqualTo(Duration.ofDays(1));
|
||||
Map<String, Object> content = invokeContributor(context.getBean(SslInfoContributor.class));
|
||||
assertThat(content).containsKey("ssl");
|
||||
assertThat(content.get("ssl")).isInstanceOf(SslInfo.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customSslInfo() {
|
||||
this.contextRunner.withUserConfiguration(CustomSslInfoConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class))
|
||||
.withPropertyValues("management.info.ssl.enabled=true", "server.ssl.bundle=ssltest",
|
||||
"spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslInfoContributor.class);
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
assertThat(context.getBean(SslInfo.class)).isSameAs(context.getBean("customSslInfo"));
|
||||
Map<String, Object> content = invokeContributor(context.getBean(SslInfoContributor.class));
|
||||
assertThat(content).containsKey("ssl");
|
||||
assertThat(content.get("ssl")).isInstanceOf(SslInfo.class);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, Object> invokeContributor(InfoContributor contributor) {
|
||||
Info.Builder builder = new Info.Builder();
|
||||
contributor.contribute(builder);
|
||||
@@ -241,4 +296,14 @@ class InfoContributorAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomSslInfoConfiguration {
|
||||
|
||||
@Bean
|
||||
SslInfo customSslInfo(SslBundles sslBundles) {
|
||||
return new SslInfo(sslBundles, Duration.ofDays(7));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.ssl.SslHealthContributorAutoConfigurationTests.CustomSslInfoConfiguration.CustomSslHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.boot.actuate.ssl.SslHealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.info.SslInfo.CertificateChain;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslHealthContributorAutoConfiguration}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class SslHealthContributorAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(SslHealthContributorAutoConfiguration.class, SslAutoConfiguration.class))
|
||||
.withPropertyValues("server.ssl.bundle=ssltest",
|
||||
"spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks");
|
||||
|
||||
@Test
|
||||
void beansShouldNotBeConfigured() {
|
||||
this.contextRunner.withPropertyValues("management.health.ssl.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(HealthIndicator.class)
|
||||
.doesNotHaveBean(SslInfo.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void beansShouldBeConfigured() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslHealthIndicator.class);
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
Health health = context.getBean(SslHealthIndicator.class).health();
|
||||
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).hasSize(1);
|
||||
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void beansShouldBeConfiguredWithWarningThreshold() {
|
||||
this.contextRunner.withPropertyValues("management.health.ssl.certificate-validity-warning-threshold=1d")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslHealthIndicator.class);
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
assertThat(context).hasSingleBean(SslHealthIndicatorProperties.class);
|
||||
assertThat(context.getBean(SslHealthIndicatorProperties.class).getCertificateValidityWarningThreshold())
|
||||
.isEqualTo(Duration.ofDays(1));
|
||||
Health health = context.getBean(SslHealthIndicator.class).health();
|
||||
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).hasSize(1);
|
||||
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customBeansShouldBeConfigured() {
|
||||
this.contextRunner.withUserConfiguration(CustomSslInfoConfiguration.class).run((context) -> {
|
||||
assertThat(context).hasSingleBean(SslHealthIndicator.class);
|
||||
assertThat(context.getBean(SslHealthIndicator.class))
|
||||
.isSameAs(context.getBean(CustomSslHealthIndicator.class));
|
||||
assertThat(context).hasSingleBean(SslInfo.class);
|
||||
assertThat(context.getBean(SslInfo.class)).isSameAs(context.getBean("customSslInfo"));
|
||||
Health health = context.getBean(SslHealthIndicator.class).health();
|
||||
assertThat(health.getStatus()).isSameAs(Status.OUT_OF_SERVICE);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).hasSize(1);
|
||||
assertThat(invalidChains).first().isInstanceOf(CertificateChain.class);
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertDetailsKeys(Health health) {
|
||||
assertThat(health.getDetails()).containsOnlyKeys("validChains", "invalidChains");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<CertificateChain> getInvalidChains(Health health) {
|
||||
return (List<CertificateChain>) health.getDetails().get("invalidChains");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomSslInfoConfiguration {
|
||||
|
||||
@Bean
|
||||
SslHealthIndicator sslHealthIndicator(SslInfo sslInfo) {
|
||||
return new CustomSslHealthIndicator(sslInfo);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SslInfo customSslInfo(SslBundles sslBundles) {
|
||||
return new SslInfo(sslBundles, Duration.ofDays(7));
|
||||
}
|
||||
|
||||
static class CustomSslHealthIndicator extends SslHealthIndicator {
|
||||
|
||||
CustomSslHealthIndicator(SslInfo sslInfo) {
|
||||
super(sslInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.info;
|
||||
|
||||
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.boot.actuate.info.Info.Builder;
|
||||
import org.springframework.boot.actuate.info.SslInfoContributor.SslInfoContributorRuntimeHints;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
|
||||
/**
|
||||
* An {@link InfoContributor} that exposes {@link SslInfo}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@ImportRuntimeHints(SslInfoContributorRuntimeHints.class)
|
||||
public class SslInfoContributor implements InfoContributor {
|
||||
|
||||
private final SslInfo sslInfo;
|
||||
|
||||
public SslInfoContributor(SslInfo sslInfo) {
|
||||
this.sslInfo = sslInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contribute(Builder builder) {
|
||||
builder.withDetail("ssl", this.sslInfo);
|
||||
}
|
||||
|
||||
static class SslInfoContributorRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
this.bindingRegistrar.registerReflectionHints(hints.reflection(), SslInfo.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health.Builder;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.info.SslInfo.CertificateChain;
|
||||
|
||||
/**
|
||||
* {@link HealthIndicator} that checks the certificates the application uses and reports
|
||||
* {@link Status#OUT_OF_SERVICE} when a certificate is invalid.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public class SslHealthIndicator extends AbstractHealthIndicator {
|
||||
|
||||
private final SslInfo sslInfo;
|
||||
|
||||
public SslHealthIndicator(SslInfo sslInfo) {
|
||||
this.sslInfo = sslInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHealthCheck(Builder builder) throws Exception {
|
||||
List<CertificateChain> certificateChains = this.sslInfo.getBundles()
|
||||
.stream()
|
||||
.flatMap((bundle) -> bundle.getCertificateChains().stream())
|
||||
.toList();
|
||||
List<CertificateChain> validCertificateChains = certificateChains.stream()
|
||||
.filter(this::containsOnlyValidCertificates)
|
||||
.toList();
|
||||
List<CertificateChain> invalidCertificateChains = certificateChains.stream()
|
||||
.filter(this::containsInvalidCertificate)
|
||||
.toList();
|
||||
builder.withDetail("validChains", validCertificateChains);
|
||||
builder.withDetail("invalidChains", invalidCertificateChains);
|
||||
if (invalidCertificateChains.isEmpty()) {
|
||||
builder.status(Status.UP);
|
||||
}
|
||||
else {
|
||||
builder.status(Status.OUT_OF_SERVICE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsOnlyValidCertificates(CertificateChain certificateChain) {
|
||||
return certificateChain.getCertificates()
|
||||
.stream()
|
||||
.filter((certificate) -> certificate.getValidity() != null)
|
||||
.allMatch((certificate) -> certificate.getValidity().getStatus().isValid());
|
||||
}
|
||||
|
||||
private boolean containsInvalidCertificate(CertificateChain certificateChain) {
|
||||
return certificateChain.getCertificates()
|
||||
.stream()
|
||||
.filter((certificate) -> certificate.getValidity() != null)
|
||||
.anyMatch((certificate) -> !certificate.getValidity().getStatus().isValid());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actuator support for ssl concerns.
|
||||
*/
|
||||
package org.springframework.boot.actuate.ssl;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.info;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.actuate.info.SslInfoContributor.SslInfoContributorRuntimeHints;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslInfoContributor}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class SslInfoContributorTests {
|
||||
|
||||
@Test
|
||||
void sslInfoShouldBeAdded() {
|
||||
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mock(SslBundle.class));
|
||||
SslInfo sslInfo = new SslInfo(sslBundles, Duration.ofDays(14));
|
||||
SslInfoContributor sslInfoContributor = new SslInfoContributor(sslInfo);
|
||||
Info.Builder builder = new Info.Builder();
|
||||
sslInfoContributor.contribute(builder);
|
||||
Info info = builder.build();
|
||||
assertThat(info.getDetails().get("ssl")).isInstanceOf(SslInfo.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
new SslInfoContributorRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.reflection()
|
||||
.onType(SslInfo.class)
|
||||
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS))
|
||||
.accepts(runtimeHints);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.boot.info.SslInfo;
|
||||
import org.springframework.boot.info.SslInfo.Bundle;
|
||||
import org.springframework.boot.info.SslInfo.CertificateChain;
|
||||
import org.springframework.boot.info.SslInfo.CertificateInfo;
|
||||
import org.springframework.boot.info.SslInfo.CertificateInfo.Validity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslHealthIndicator}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class SslHealthIndicatorTests {
|
||||
|
||||
private HealthIndicator healthIndicator;
|
||||
|
||||
private Validity validity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
SslInfo sslInfo = mock(SslInfo.class);
|
||||
Bundle bundle = mock(Bundle.class);
|
||||
CertificateChain certificateChain = mock(CertificateChain.class);
|
||||
CertificateInfo certificateInfo = mock(CertificateInfo.class);
|
||||
|
||||
this.healthIndicator = new SslHealthIndicator(sslInfo);
|
||||
this.validity = mock(Validity.class);
|
||||
|
||||
given(sslInfo.getBundles()).willReturn(List.of(bundle));
|
||||
given(bundle.getCertificateChains()).willReturn(List.of(certificateChain));
|
||||
given(certificateChain.getCertificates()).willReturn(List.of(certificateInfo));
|
||||
given(certificateInfo.getValidity()).willReturn(this.validity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeUpIfNoSslIssuesDetected() {
|
||||
given(this.validity.getStatus()).willReturn(Validity.Status.VALID);
|
||||
Health health = this.healthIndicator.health();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> validChains = getValidChains(health);
|
||||
assertThat(validChains).hasSize(1);
|
||||
assertThat(validChains.get(0)).isInstanceOf(CertificateChain.class);
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeOutOfServiceIfACertificateIsExpired() {
|
||||
given(this.validity.getStatus()).willReturn(Validity.Status.EXPIRED);
|
||||
Health health = this.healthIndicator.health();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> validChains = getValidChains(health);
|
||||
assertThat(validChains).isEmpty();
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).hasSize(1);
|
||||
assertThat(invalidChains.get(0)).isInstanceOf(CertificateChain.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeOutOfServiceIfACertificateIsNotYetValid() {
|
||||
given(this.validity.getStatus()).willReturn(Validity.Status.NOT_YET_VALID);
|
||||
Health health = this.healthIndicator.health();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> validChains = getValidChains(health);
|
||||
assertThat(validChains).isEmpty();
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).hasSize(1);
|
||||
assertThat(invalidChains.get(0)).isInstanceOf(CertificateChain.class);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportWarningIfACertificateWillExpireSoon() {
|
||||
given(this.validity.getStatus()).willReturn(Validity.Status.WILL_EXPIRE_SOON);
|
||||
Health health = this.healthIndicator.health();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
assertDetailsKeys(health);
|
||||
List<CertificateChain> validChains = getValidChains(health);
|
||||
assertThat(validChains).hasSize(1);
|
||||
assertThat(validChains.get(0)).isInstanceOf(CertificateChain.class);
|
||||
List<CertificateChain> invalidChains = getInvalidChains(health);
|
||||
assertThat(invalidChains).isEmpty();
|
||||
}
|
||||
|
||||
private static void assertDetailsKeys(Health health) {
|
||||
assertThat(health.getDetails()).containsOnlyKeys("validChains", "invalidChains");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<CertificateChain> getInvalidChains(Health health) {
|
||||
return (List<CertificateChain>) health.getDetails().get("invalidChains");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<CertificateChain> getValidChains(Health health) {
|
||||
return (List<CertificateChain>) health.getDetails().get("validChains");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -648,10 +648,20 @@ with the `key` listed in the following table:
|
||||
| `redis`
|
||||
| javadoc:org.springframework.boot.actuate.data.redis.RedisHealthIndicator[]
|
||||
| Checks that a Redis server is up.
|
||||
|
||||
| `ssl`
|
||||
| javadoc:org.springframework.boot.actuate.ssl.SslHealthIndicator[]
|
||||
| Checks that SSL Certificates are ok.
|
||||
|===
|
||||
|
||||
TIP: You can disable them all by setting the configprop:management.health.defaults.enabled[] property.
|
||||
|
||||
TIP: The `ssl` `HealthIndicator` has a "warning threshold" property named configprop:management.health.ssl.certificate-validity-warning-threshold[].
|
||||
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.
|
||||
|
||||
|
||||
|
||||
Additional `HealthIndicators` are available but are not enabled by default:
|
||||
|
||||
[cols="3,4,6"]
|
||||
@@ -1110,12 +1120,17 @@ When appropriate, Spring auto-configures the following `InfoContributor` beans:
|
||||
| Exposes process information.
|
||||
| None.
|
||||
|
||||
| `ssl`
|
||||
| javadoc:org.springframework.boot.actuate.info.SslInfoContributor[]
|
||||
| Exposes SSL Certificate information.
|
||||
| An xref:features/ssl.adoc#features.ssl.bundles[SSL Bundle] configured.
|
||||
|
||||
|===
|
||||
|
||||
Whether an individual contributor is enabled is controlled by its `management.info.<id>.enabled` property.
|
||||
Different contributors have different defaults for this property, depending on their prerequisites and the nature of the information that they expose.
|
||||
|
||||
With no prerequisites to indicate that they should be enabled, the `env`, `java`, `os`, and `process` contributors are disabled by default.
|
||||
With no prerequisites to indicate that they should be enabled, the `env`, `java`, `os`, and `process` contributors are disabled by default. The `ssl` contributor has a prerequisite of having an xref:features/ssl.adoc#features.ssl.bundles[SSL Bundle] configured but it is disabled by default.
|
||||
Each can be enabled by setting its `management.info.<id>.enabled` property to `true`.
|
||||
|
||||
The `build` and `git` info contributors are enabled by default.
|
||||
@@ -1225,6 +1240,13 @@ The `info` endpoint publishes information about your process, see javadoc:org.sp
|
||||
|
||||
|
||||
|
||||
[[actuator.endpoints.info.ssl-information]]
|
||||
=== SSL Information
|
||||
|
||||
The `info` endpoint publishes information about your SSL Certificates (that are configured through xref:features/ssl.adoc#features.ssl.bundles[SSL Bundles]), see javadoc:org.springframework.boot.info.SslInfo[] for more details. This endpoint reuses the "warning threshold" property of javadoc:org.springframework.boot.actuate.ssl.SslHealthIndicator[]: if an SSL Certificate will be invalid within the time span defined by this threshold, it will trigger a warning. See the `management.health.ssl.certificate-validity-warning-threshold` property.
|
||||
|
||||
|
||||
|
||||
[[actuator.endpoints.info.writing-custom-info-contributors]]
|
||||
=== Writing Custom InfoContributors
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.info;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
|
||||
/**
|
||||
* Information about the certificates that the application uses.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public class SslInfo {
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
private final Duration certificateValidityWarningThreshold;
|
||||
|
||||
public SslInfo(SslBundles sslBundles, Duration certificateValidityWarningThreshold) {
|
||||
this.sslBundles = sslBundles;
|
||||
this.certificateValidityWarningThreshold = certificateValidityWarningThreshold;
|
||||
}
|
||||
|
||||
public List<Bundle> getBundles() {
|
||||
return this.sslBundles.getBundles()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map((entry) -> new Bundle(entry.getKey(), entry.getValue()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public final class Bundle {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final List<CertificateChain> certificateChains;
|
||||
|
||||
private Bundle(String name, SslBundle sslBundle) {
|
||||
this.name = name;
|
||||
this.certificateChains = createCertificateChains(sslBundle.getStores().getKeyStore());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public List<CertificateChain> getCertificateChains() {
|
||||
return this.certificateChains;
|
||||
}
|
||||
|
||||
private List<CertificateChain> createCertificateChains(KeyStore keyStore) {
|
||||
try {
|
||||
return Collections.list(keyStore.aliases())
|
||||
.stream()
|
||||
.map((alias) -> new CertificateChain(alias, getCertificates(alias, keyStore)))
|
||||
.toList();
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Certificate> getCertificates(String alias, KeyStore keyStore) {
|
||||
try {
|
||||
Certificate[] certificateChain = keyStore.getCertificateChain(alias);
|
||||
return (certificateChain != null) ? List.of(certificateChain) : Collections.emptyList();
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final class CertificateChain {
|
||||
|
||||
private final String alias;
|
||||
|
||||
private final List<CertificateInfo> certificates;
|
||||
|
||||
CertificateChain(String alias, List<Certificate> certificates) {
|
||||
this.alias = alias;
|
||||
this.certificates = certificates.stream().map(CertificateInfo::new).toList();
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
public List<CertificateInfo> getCertificates() {
|
||||
return this.certificates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final class CertificateInfo {
|
||||
|
||||
private final X509Certificate certificate;
|
||||
|
||||
private CertificateInfo(Certificate certificate) {
|
||||
if (certificate instanceof X509Certificate x509Certificate) {
|
||||
this.certificate = x509Certificate;
|
||||
}
|
||||
else {
|
||||
this.certificate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return (this.certificate != null) ? this.certificate.getSubjectX500Principal().getName() : null;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return (this.certificate != null) ? this.certificate.getIssuerX500Principal().getName() : null;
|
||||
}
|
||||
|
||||
public String getSerialNumber() {
|
||||
return (this.certificate != null) ? this.certificate.getSerialNumber().toString(16) : null;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return (this.certificate != null) ? "V" + this.certificate.getVersion() : null;
|
||||
}
|
||||
|
||||
public String getSignatureAlgorithmName() {
|
||||
return (this.certificate != null) ? this.certificate.getSigAlgName() : null;
|
||||
}
|
||||
|
||||
public Instant getValidityStarts() {
|
||||
return (this.certificate != null) ? this.certificate.getNotBefore().toInstant() : null;
|
||||
}
|
||||
|
||||
public Instant getValidityEnds() {
|
||||
return (this.certificate != null) ? this.certificate.getNotAfter().toInstant() : null;
|
||||
}
|
||||
|
||||
public Validity getValidity() {
|
||||
try {
|
||||
if (this.certificate != null) {
|
||||
this.certificate.checkValidity();
|
||||
if (isCloseToBeExpired(this.certificate, SslInfo.this.certificateValidityWarningThreshold)) {
|
||||
return new Validity(Status.WILL_EXPIRE_SOON,
|
||||
"Certificate will expire within threshold (%s) at %s".formatted(
|
||||
SslInfo.this.certificateValidityWarningThreshold, this.getValidityEnds()));
|
||||
}
|
||||
else {
|
||||
return new Validity(Status.VALID, null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (CertificateNotYetValidException exception) {
|
||||
return new Validity(Status.NOT_YET_VALID, "Not valid before %s".formatted(this.getValidityStarts()));
|
||||
}
|
||||
catch (CertificateExpiredException exception) {
|
||||
return new Validity(Status.EXPIRED, "Not valid after %s".formatted(this.getValidityEnds()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCloseToBeExpired(X509Certificate certificate, Duration certificateValidityThreshold) {
|
||||
Instant shouldBeValidAt = Instant.now().plus(certificateValidityThreshold);
|
||||
Instant expiresAt = certificate.getNotAfter().toInstant();
|
||||
return shouldBeValidAt.isAfter(expiresAt);
|
||||
}
|
||||
|
||||
public static class Validity {
|
||||
|
||||
private final Status status;
|
||||
|
||||
private final String message;
|
||||
|
||||
Validity(Status status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
|
||||
/**
|
||||
* The certificate is valid.
|
||||
*/
|
||||
VALID(true),
|
||||
|
||||
/**
|
||||
* The certificate's validity date range is in the future.
|
||||
*/
|
||||
NOT_YET_VALID(false),
|
||||
|
||||
/**
|
||||
* The certificate's validity date range is in the past.
|
||||
*/
|
||||
EXPIRED(false),
|
||||
|
||||
/**
|
||||
* The certificate is still valid, but the end of its validity date range
|
||||
* is within the defined threshold.
|
||||
*/
|
||||
WILL_EXPIRE_SOON(true);
|
||||
|
||||
private final boolean valid;
|
||||
|
||||
Status(boolean valid) {
|
||||
this.valid = valid;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,9 +18,11 @@ package org.springframework.boot.ssl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -34,6 +36,7 @@ import org.springframework.util.Assert;
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
* @author Phillip Webb
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
|
||||
@@ -67,6 +70,13 @@ public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
|
||||
return getRegistered(name).getBundle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, SslBundle> getBundles() {
|
||||
return this.registeredBundles.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toUnmodifiableMap(Entry::getKey, (entry) -> entry.getValue().getBundle()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBundleUpdateHandler(String name, Consumer<SslBundle> updateHandler) throws NoSuchSslBundleException {
|
||||
getRegistered(name).addUpdateHandler(updateHandler);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.ssl;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ import java.util.function.Consumer;
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
* @author Jonatan Ivanov
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public interface SslBundles {
|
||||
@@ -35,6 +37,13 @@ public interface SslBundles {
|
||||
*/
|
||||
SslBundle getBundle(String name) throws NoSuchSslBundleException;
|
||||
|
||||
/**
|
||||
* Return all the {@link SslBundle SslBundles} by name.
|
||||
* @return the bundles
|
||||
* @since 3.4.0
|
||||
*/
|
||||
Map<String, SslBundle> getBundles();
|
||||
|
||||
/**
|
||||
* Add a handler that will be called each time the named bundle is updated.
|
||||
* @param name the bundle name
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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.info;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.info.SslInfo.Bundle;
|
||||
import org.springframework.boot.info.SslInfo.CertificateChain;
|
||||
import org.springframework.boot.info.SslInfo.CertificateInfo;
|
||||
import org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
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 SslInfo}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class SslInfoTests {
|
||||
|
||||
@Test
|
||||
void validCertificatesShouldProvideSslInfo() {
|
||||
SslInfo sslInfo = createSslInfo("classpath:test.p12");
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
Bundle bundle = sslInfo.getBundles().get(0);
|
||||
assertThat(bundle.getName()).isEqualTo("test-0");
|
||||
assertThat(bundle.getCertificateChains()).hasSize(4);
|
||||
assertThat(bundle.getCertificateChains().get(0).getAlias()).isEqualTo("spring-boot");
|
||||
assertThat(bundle.getCertificateChains().get(0).getCertificates()).hasSize(1);
|
||||
assertThat(bundle.getCertificateChains().get(1).getAlias()).isEqualTo("test-alias");
|
||||
assertThat(bundle.getCertificateChains().get(1).getCertificates()).hasSize(1);
|
||||
assertThat(bundle.getCertificateChains().get(2).getAlias()).isEqualTo("spring-boot-cert");
|
||||
assertThat(bundle.getCertificateChains().get(2).getCertificates()).isEmpty();
|
||||
assertThat(bundle.getCertificateChains().get(3).getAlias()).isEqualTo("test-alias-cert");
|
||||
assertThat(bundle.getCertificateChains().get(3).getCertificates()).isEmpty();
|
||||
|
||||
CertificateInfo cert1 = bundle.getCertificateChains().get(0).getCertificates().get(0);
|
||||
assertThat(cert1.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert1.getIssuer()).isEqualTo(cert1.getSubject());
|
||||
assertThat(cert1.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert1.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert1.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert1.getValidityStarts()).isInThePast();
|
||||
assertThat(cert1.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert1.getValidity()).isNotNull();
|
||||
assertThat(cert1.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert1.getValidity().getMessage()).isNull();
|
||||
|
||||
CertificateInfo cert2 = bundle.getCertificateChains().get(1).getCertificates().get(0);
|
||||
assertThat(cert2.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert2.getIssuer()).isEqualTo(cert2.getSubject());
|
||||
assertThat(cert2.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert2.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert2.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert2.getValidityStarts()).isInThePast();
|
||||
assertThat(cert2.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert2.getValidity()).isNotNull();
|
||||
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert2.getValidity().getMessage()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void notYetValidCertificateShouldProvideSslInfo() {
|
||||
SslInfo sslInfo = createSslInfo("classpath:test-not-yet-valid.p12");
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
Bundle bundle = sslInfo.getBundles().get(0);
|
||||
assertThat(bundle.getName()).isEqualTo("test-0");
|
||||
assertThat(bundle.getCertificateChains()).hasSize(1);
|
||||
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
|
||||
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
|
||||
List<CertificateInfo> certs = certificateChain.getCertificates();
|
||||
assertThat(certs).hasSize(1);
|
||||
CertificateInfo cert = certs.get(0);
|
||||
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert.getValidityStarts()).isInTheFuture();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredCertificateShouldProvideSslInfo() {
|
||||
SslInfo sslInfo = createSslInfo("classpath:test-expired.p12");
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
Bundle bundle = sslInfo.getBundles().get(0);
|
||||
assertThat(bundle.getName()).isEqualTo("test-0");
|
||||
assertThat(bundle.getCertificateChains()).hasSize(1);
|
||||
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
|
||||
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
|
||||
List<CertificateInfo> certs = certificateChain.getCertificates();
|
||||
assertThat(certs).hasSize(1);
|
||||
CertificateInfo cert = certs.get(0);
|
||||
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInThePast();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
|
||||
}
|
||||
|
||||
@Test
|
||||
void soonToBeExpiredCertificateShouldProvideSslInfo(@TempDir Path tempDir)
|
||||
throws IOException, InterruptedException {
|
||||
Path keyStore = createKeyStore(tempDir);
|
||||
SslInfo sslInfo = createSslInfo(keyStore.toString());
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
Bundle bundle = sslInfo.getBundles().get(0);
|
||||
assertThat(bundle.getName()).isEqualTo("test-0");
|
||||
assertThat(bundle.getCertificateChains()).hasSize(1);
|
||||
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
|
||||
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
|
||||
List<CertificateInfo> certs = certificateChain.getCertificates();
|
||||
assertThat(certs).hasSize(1);
|
||||
CertificateInfo cert = certs.get(0);
|
||||
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isNotEmpty();
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.WILL_EXPIRE_SOON);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Certificate will expire within threshold");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOException, InterruptedException {
|
||||
Path keyStore = createKeyStore(tempDir);
|
||||
SslInfo sslInfo = createSslInfo("classpath:test.p12", "classpath:test-not-yet-valid.p12",
|
||||
"classpath:test-expired.p12", keyStore.toString());
|
||||
assertThat(sslInfo.getBundles()).hasSize(4);
|
||||
assertThat(sslInfo.getBundles()).allSatisfy((bundle) -> assertThat(bundle.getName()).startsWith("test-"));
|
||||
|
||||
List<CertificateInfo> certs = sslInfo.getBundles()
|
||||
.stream()
|
||||
.flatMap((bundle) -> bundle.getCertificateChains().stream())
|
||||
.flatMap((certificateChain) -> certificateChain.getCertificates().stream())
|
||||
.toList();
|
||||
|
||||
assertThat(certs).hasSize(5);
|
||||
assertThat(certs).allSatisfy((cert) -> {
|
||||
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
|
||||
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isNotEmpty();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
});
|
||||
|
||||
assertThat(certs).anySatisfy((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert.getValidity().getMessage()).isNull();
|
||||
});
|
||||
|
||||
assertThat(certs).satisfiesOnlyOnce((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInTheFuture();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
|
||||
});
|
||||
|
||||
assertThat(certs).satisfiesOnlyOnce((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInThePast();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
|
||||
});
|
||||
|
||||
assertThat(certs).satisfiesOnlyOnce((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.WILL_EXPIRE_SOON);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Certificate will expire within threshold");
|
||||
});
|
||||
}
|
||||
|
||||
private SslInfo createSslInfo(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 new SslInfo(sslBundleRegistry, Duration.ofDays(7));
|
||||
}
|
||||
|
||||
private Path createKeyStore(Path directory) throws IOException, InterruptedException {
|
||||
Path keyStore = directory.resolve("test.p12");
|
||||
Process process = createProcessBuilder(keyStore).start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
String out = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
throw new RuntimeException("Unexpected exit code from keytool: %d\n%s".formatted(exitCode, out));
|
||||
}
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
private ProcessBuilder createProcessBuilder(Path keystore) {
|
||||
// @formatter:off
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(
|
||||
"keytool",
|
||||
"-genkeypair",
|
||||
"-storetype", "PKCS12",
|
||||
"-alias", "spring-boot",
|
||||
"-keyalg", "RSA",
|
||||
"-storepass", "secret",
|
||||
"-keypass", "secret",
|
||||
"-keystore", keystore.toString(),
|
||||
"-dname", "CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US",
|
||||
"-validity", "1",
|
||||
"-ext", "SAN=DNS:localhost,IP:::1,IP:127.0.0.1"
|
||||
);
|
||||
// @formatter:on
|
||||
processBuilder.redirectErrorStream(true);
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -105,6 +105,15 @@ class DefaultSslBundleRegistryTests {
|
||||
assertThat(this.registry.getBundle("test2")).isSameAs(this.bundle2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBundlesReturnsBundles() {
|
||||
this.registry.registerBundle("test1", this.bundle1);
|
||||
this.registry.registerBundle("test2", this.bundle2);
|
||||
assertThat(this.registry.getBundles()).hasSize(2)
|
||||
.containsEntry("test1", this.bundle1)
|
||||
.containsEntry("test2", this.bundle2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateBundleShouldNotifyUpdateHandlers() {
|
||||
AtomicReference<SslBundle> updatedBundle = new AtomicReference<>();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,7 @@ description = "Spring Boot Tomcat SSL smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
|
||||
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
testImplementation("org.apache.httpcomponents.client5:httpclient5")
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
server.port = 8443
|
||||
server.ssl.key-store = classpath:sample.jks
|
||||
server.ssl.key-store-password = secret
|
||||
server.ssl.key-password = password
|
||||
server.port=8443
|
||||
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.health.show-details=always
|
||||
management.health.ssl.certificate-validity-warning-threshold=7d
|
||||
management.health.ssl.enabled=true
|
||||
management.info.ssl.enabled=true
|
||||
|
||||
server.ssl.bundle=ssldemo
|
||||
spring.ssl.bundle.jks.ssldemo.keystore.location=classpath:sample.jks
|
||||
spring.ssl.bundle.jks.ssldemo.keystore.password=secret
|
||||
spring.ssl.bundle.jks.ssldemo.keystore.type=JKS
|
||||
spring.ssl.bundle.jks.ssldemo.key.password=password
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,6 +25,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.json.JsonContent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -49,4 +50,43 @@ class SampleTomcatSslApplicationTests {
|
||||
assertThat(entity.getBody()).isEqualTo("Hello, world");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSslInfo() {
|
||||
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/info", String.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
JsonContent body = new JsonContent(entity.getBody());
|
||||
assertThat(body).extractingPath("ssl.bundles[0].name").isEqualTo("ssldemo");
|
||||
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].alias")
|
||||
.isEqualTo("spring-boot-ssl-sample");
|
||||
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].issuer")
|
||||
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
|
||||
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].subject")
|
||||
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
|
||||
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].validity.status")
|
||||
.isEqualTo("EXPIRED");
|
||||
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].validity.message")
|
||||
.asString()
|
||||
.startsWith("Not valid after ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSslHealth() {
|
||||
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
JsonContent body = new JsonContent(entity.getBody());
|
||||
assertThat(body).extractingPath("status").isEqualTo("OUT_OF_SERVICE");
|
||||
assertThat(body).extractingPath("components.ssl.status").isEqualTo("OUT_OF_SERVICE");
|
||||
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].alias")
|
||||
.isEqualTo("spring-boot-ssl-sample");
|
||||
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].issuer")
|
||||
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
|
||||
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].subject")
|
||||
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
|
||||
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].validity.status")
|
||||
.isEqualTo("EXPIRED");
|
||||
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].validity.message")
|
||||
.asString()
|
||||
.startsWith("Not valid after ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user