From 0ef93b2113d30a565c285a89ecd220e24cb9ed79 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Thu, 16 Nov 2023 15:33:57 -0800 Subject: [PATCH] Add SslInfoContributor and SslHealthIndicator --- .../InfoContributorAutoConfiguration.java | 21 ++ ...SslHealthContributorAutoConfiguration.java | 57 +++++ .../autoconfigure/ssl/package-info.java | 20 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../boot/actuate/info/SslInfoContributor.java | 41 +++ .../boot/actuate/ssl/SslHealthIndicator.java | 88 +++++++ .../boot/actuate/ssl/package-info.java | 20 ++ .../springframework/boot/info/SslInfo.java | 239 ++++++++++++++++++ .../boot/ssl/DefaultSslBundleRegistry.java | 12 +- .../springframework/boot/ssl/SslBundles.java | 11 +- .../build.gradle | 1 + .../src/main/resources/application.properties | 18 +- 12 files changed, 523 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/SslInfoContributor.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/package-info.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java index b26455ad43a..f7623d01d02 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.info; +import java.time.Duration; + import org.springframework.boot.actuate.info.BuildInfoContributor; import org.springframework.boot.actuate.info.EnvironmentInfoContributor; import org.springframework.boot.actuate.info.GitInfoContributor; @@ -23,14 +25,18 @@ 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; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; 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; @@ -100,4 +106,19 @@ public class InfoContributorAutoConfiguration { return new ProcessInfoContributor(); } + @Bean + @ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE) + @Order(DEFAULT_ORDER) + public SslInfoContributor sslInfoContributor(SslInfo sslInfo) { + return new SslInfoContributor(sslInfo); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledInfoContributor(value = "ssl", fallback = InfoContributorFallback.DISABLE) + public SslInfo sslInfo(ServerProperties serverProperties, SslBundles sslBundles) { + // TODO: Get the certificateValidityThreshold from a property + return new SslInfo(serverProperties.getSsl(), sslBundles, Duration.ofDays(7)); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfiguration.java new file mode 100644 index 00000000000..6a2a5000f30 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/SslHealthContributorAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * 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.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.autoconfigure.web.ServerProperties; +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") +public class SslHealthContributorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "sslHealthIndicator") + public SslHealthIndicator sslHealthIndicator(SslInfo sslInfo) { + return new SslHealthIndicator(sslInfo); + } + + @Bean + @ConditionalOnMissingBean + public SslInfo sslInfo(ServerProperties serverProperties, SslBundles sslBundles) { + // TODO: Get the certificateValidityThreshold from a property + // TODO: This is the same as the one in InfoContributorAutoConfiguration, + // should we keep just one? + return new SslInfo(serverProperties.getSsl(), sslBundles, Duration.ofDays(7)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/package-info.java new file mode 100644 index 00000000000..bfeaa736f6b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/ssl/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d3a6fc7ead5..29e797d7f60 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -103,6 +103,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 diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/SslInfoContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/SslInfoContributor.java new file mode 100644 index 00000000000..e328ef8ea45 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/SslInfoContributor.java @@ -0,0 +1,41 @@ +/* + * 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.boot.actuate.info.Info.Builder; +import org.springframework.boot.info.SslInfo; + +/** + * An {@link InfoContributor} that exposes {@link SslInfo}. + * + * @author Jonatan Ivanov + * @since 3.4.0 + */ +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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java new file mode 100644 index 00000000000..de5bf9261c4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java @@ -0,0 +1,88 @@ +/* + * 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 java.util.Set; +import java.util.stream.Collectors; + +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.CertificateInfo; +import org.springframework.boot.info.SslInfo.CertificateInfo.Validity; + +import static org.springframework.boot.actuate.health.Status.OUT_OF_SERVICE; +import static org.springframework.boot.actuate.health.Status.UP; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.EXPIRED; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.NOT_YET_VALID; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.VALID; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.WILL_EXPIRE_SOON; + +/** + * {@link HealthIndicator} that checks the certificates the application uses and reports + * {@link Status#OUT_OF_SERVICE} when a certificate is invalid or "WILL_EXPIRE_SOON" if it + * will expire within the configurable threshold. + * + * @author Jonatan Ivanov + * @since 3.4.0 + */ +public class SslHealthIndicator extends AbstractHealthIndicator { + + private static final Status WILL_EXPIRE_SOON_STATUS = new Status(WILL_EXPIRE_SOON.name(), + "One of the certificates will expire within the defined threshold."); + + private final SslInfo sslInfo; + + public SslHealthIndicator(SslInfo sslInfo) { + this.sslInfo = sslInfo; + } + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + List notValidCertificates = this.sslInfo.getBundles() + .stream() + .flatMap(bundle -> bundle.getCertificateChains().stream()) + .flatMap(certificateChain -> certificateChain.getCertificates().stream()) + .filter(certificate -> certificate.getValidity().getStatus() != VALID) + .toList(); + + if (notValidCertificates.isEmpty()) { + builder.status(UP); + } + else { + Set statuses = notValidCertificates.stream() + .map(certificate -> certificate.getValidity().getStatus()) + .collect(Collectors.toUnmodifiableSet()); + if (statuses.contains(EXPIRED) || statuses.contains(NOT_YET_VALID)) { + builder.status(OUT_OF_SERVICE); + } + else if (statuses.contains(WILL_EXPIRE_SOON)) { + // TODO: Should we introduce Status.WARNING + // (returns 200 but indicates that something is not right)? + builder.status(WILL_EXPIRE_SOON_STATUS); + } + else { + builder.status(OUT_OF_SERVICE); + } + builder.withDetail("certificates", notValidCertificates); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/package-info.java new file mode 100644 index 00000000000..a4296abf5e2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java new file mode 100644 index 00000000000..b707b0cc2e4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java @@ -0,0 +1,239 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.WebServerSslBundle; + +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.EXPIRED; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.NOT_YET_VALID; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.VALID; +import static org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status.WILL_EXPIRE_SOON; + +/** + * Information about the certificates that the application uses. + * + * @author Jonatan Ivanov + * @since 3.4.0 + */ +public class SslInfo { + + private final List bundles; + + private final Duration certificateValidityThreshold; + + public SslInfo(Ssl ssl, SslBundles sslBundles, Duration certificateValidityThreshold) { + List bundles = new ArrayList<>(); + for (Entry entry : sslBundles.getBundles().entrySet()) { + bundles.add(new Bundle(entry.getKey(), entry.getValue())); + } + if (ssl.getBundle() == null) { + // TODO: WebServerSslBundle.get is called at multiple places + // (i.e.: in AbstractConfigurableWebServerFactory#getSslBundle) + // so this is a duplicate, can we create one instance and reuse it + // (e.g.: a bean) or integrate it with SslBundles + // that would make this block unnecessary? + bundles.add(new Bundle("webServerSslBundle", WebServerSslBundle.get(ssl, sslBundles))); + } + this.bundles = Collections.unmodifiableList(bundles); + this.certificateValidityThreshold = certificateValidityThreshold; + } + + public List getBundles() { + return this.bundles; + } + + public class Bundle { + + private final String name; + + private final List certificateChains; + + private Bundle(String name, SslBundle sslBundle) { + this.name = name; + this.certificateChains = createCertificateChains(sslBundle.getStores().getKeyStore()); + } + + public String getName() { + return this.name; + } + + public List getCertificateChains() { + return this.certificateChains; + } + + private List createCertificateChains(KeyStore keyStore) { + try { + return Collections.list(keyStore.aliases()) + .stream() + .map(alias -> new CertificateChain(alias, getCertificates(alias, keyStore))) + .toList(); + } + catch (KeyStoreException e) { + return List.of(); + } + } + + private List getCertificates(String alias, KeyStore keyStore) { + try { + return List.of(keyStore.getCertificateChain(alias)); + } + catch (KeyStoreException e) { + return List.of(); + } + } + + } + + public class CertificateChain { + + private final String alias; + + private final List certificates; + + CertificateChain(String alias, List certificates) { + this.alias = alias; + this.certificates = certificates.stream().map(CertificateInfo::new).toList(); + } + + public String getAlias() { + return this.alias; + } + + public List getCertificates() { + return this.certificates; + } + + } + + public class CertificateInfo { + + private final X509Certificate certificate; + + private CertificateInfo(Certificate certificate) { + // TODO: Is supporting X509Certificate enough? (I _assume_ yes) + 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.certificateValidityThreshold)) { + return new Validity(WILL_EXPIRE_SOON, "Certificate will expire within threshold (%s) at %s" + .formatted(SslInfo.this.certificateValidityThreshold, this.getValidityEnds())); + } + else { + return new Validity(VALID, null); + } + } + else { + return null; + } + } + catch (CertificateNotYetValidException exception) { + return new Validity(NOT_YET_VALID, "Not valid before %s".formatted(this.getValidityStarts())); + } + catch (CertificateExpiredException exception) { + return new Validity(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 { + + VALID, NOT_YET_VALID, EXPIRED, WILL_EXPIRE_SOON + + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java index 8c999e5ccf4..56686ed781f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java @@ -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 getBundles() { + return this.registeredBundles.entrySet() + .stream() + .collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> entry.getValue().getBundle())); + } + @Override public void addBundleUpdateHandler(String name, Consumer updateHandler) throws NoSuchSslBundleException { getRegistered(name).addUpdateHandler(updateHandler); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java index 21afc4346a6..d6c52706622 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java @@ -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 getBundles(); + /** * Add a handler that will be called each time the named bundle is updated. * @param name the bundle name diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/build.gradle index 6f7cd289554..a2257efe5d4 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/build.gradle @@ -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") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/src/main/resources/application.properties index 37199bfd256..06912170637 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl/src/main/resources/application.properties @@ -1,4 +1,14 @@ -server.port = 8443 -server.ssl.key-store = classpath:sample.jks -server.ssl.key-store-password = secret -server.ssl.key-password = password +server.port=8443 +#server.ssl.key-store=classpath:sample.jks +#server.ssl.key-store-password=secret +#server.ssl.key-password=password + +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +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