mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Retain SSL host config customizations when reloading SSL bundles
Tomcat's SSL configuration is applied to an `SSLHostConfig` for each host name. When an SSL bundle was updated, a new `SSLHostConfig` was created and used to replace the existing one for that host name, discarding any customizations that had been applied to it (for example by a `TomcatConnectorCustomizer`). Reuse the existing `SSLHostConfig` for the host name when one is present, applying the updated bundle to it rather than replacing it. The existing `SSLHostConfigCertificate` is also reused, as adding a second certificate with an undefined type to an `SSLHostConfig` is rejected by Tomcat. See gh-51290 Signed-off-by: Scott Frederick <scottyfred@gmail.com>
This commit is contained in:
committed by
Phillip Webb
parent
5a21e0d698
commit
cbbe588d1f
+28
-3
@@ -19,6 +19,7 @@ package org.springframework.boot.tomcat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -66,7 +67,13 @@ public class SslConnectorCustomizer {
|
||||
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) this.connector.getProtocolHandler();
|
||||
String host = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName();
|
||||
this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration");
|
||||
addSslHostConfig(protocol, host, updatedSslBundle);
|
||||
SSLHostConfig sslHostConfig = findSslHostConfig(protocol, host);
|
||||
if (sslHostConfig == null) {
|
||||
addSslHostConfig(protocol, host, updatedSslBundle);
|
||||
return;
|
||||
}
|
||||
applySslBundle(protocol, sslHostConfig, updatedSslBundle);
|
||||
protocol.addSslHostConfig(sslHostConfig, true);
|
||||
}
|
||||
|
||||
public void customize(SslBundle sslBundle, Map<String, SslBundle> serverNameSslBundles) {
|
||||
@@ -101,12 +108,21 @@ public class SslConnectorCustomizer {
|
||||
protocol.addSslHostConfig(sslHostConfig, true);
|
||||
}
|
||||
|
||||
private @Nullable SSLHostConfig findSslHostConfig(AbstractHttp11Protocol<?> protocol, String serverName) {
|
||||
for (SSLHostConfig candidate : protocol.findSslHostConfigs()) {
|
||||
if (serverName.equalsIgnoreCase(candidate.getHostName())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applySslBundle(AbstractHttp11Protocol<?> protocol, SSLHostConfig sslHostConfig, SslBundle sslBundle) {
|
||||
SslBundleKey key = sslBundle.getKey();
|
||||
SslStoreBundle stores = sslBundle.getStores();
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
sslHostConfig.setSslProtocol(sslBundle.getProtocol());
|
||||
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
|
||||
SSLHostConfigCertificate certificate = getCertificate(sslHostConfig);
|
||||
String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : "";
|
||||
certificate.setCertificateKeystorePassword(keystorePassword);
|
||||
if (key.getPassword() != null) {
|
||||
@@ -115,12 +131,21 @@ public class SslConnectorCustomizer {
|
||||
if (key.getAlias() != null) {
|
||||
certificate.setCertificateKeyAlias(key.getAlias());
|
||||
}
|
||||
sslHostConfig.addCertificate(certificate);
|
||||
configureCiphers(options, sslHostConfig);
|
||||
configureSslStores(sslHostConfig, certificate, stores);
|
||||
configureEnabledProtocols(sslHostConfig, options);
|
||||
}
|
||||
|
||||
private SSLHostConfigCertificate getCertificate(SSLHostConfig sslHostConfig) {
|
||||
Set<SSLHostConfigCertificate> certificates = sslHostConfig.getCertificates();
|
||||
if (certificates.size() == 1) {
|
||||
return certificates.iterator().next();
|
||||
}
|
||||
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
|
||||
sslHostConfig.addCertificate(certificate);
|
||||
return certificate;
|
||||
}
|
||||
|
||||
private void configureCiphers(SslOptions options, SSLHostConfig sslHostConfig) {
|
||||
CipherConfiguration cipherConfiguration = CipherConfiguration.from(options);
|
||||
if (cipherConfiguration != null) {
|
||||
|
||||
+42
@@ -22,6 +22,7 @@ import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
import org.apache.tomcat.util.net.SSLHostConfig;
|
||||
import org.apache.tomcat.util.net.openssl.ciphers.Cipher;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -157,6 +158,47 @@ class SslConnectorCustomizerTests {
|
||||
assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void updateRetainsCustomizationsAppliedToSslHostConfig() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0];
|
||||
sslHostConfig.setTruststoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
customizer.update(null, WebServerSslBundle.get(ssl));
|
||||
assertThat(protocol.findSslHostConfigs()).hasSize(1);
|
||||
SSLHostConfig updated = protocol.findSslHostConfigs()[0];
|
||||
assertThat(updated.getTruststoreProvider()).isEqualTo(MockPkcs11SecurityProvider.NAME);
|
||||
assertThat(updated.getCertificates()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void updateAppliesUpdatedBundleToExistingSslHostConfig() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.2" });
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
this.tomcat.start();
|
||||
assertThat(protocol.findSslHostConfigs()[0].getEnabledProtocols()).containsExactly("TLSv1.2");
|
||||
Ssl updatedSsl = new Ssl();
|
||||
updatedSsl.setKeyPassword("password");
|
||||
updatedSsl.setKeyStore("classpath:test.jks");
|
||||
updatedSsl.setEnabledProtocols(new String[] { "TLSv1.3" });
|
||||
customizer.update(null, WebServerSslBundle.get(updatedSsl));
|
||||
SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0];
|
||||
assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
|
||||
+28
@@ -72,6 +72,7 @@ import org.apache.hc.core5.ssl.SSLContextBuilder;
|
||||
import org.apache.jasper.servlet.JspServlet;
|
||||
import org.apache.tomcat.JarScanFilter;
|
||||
import org.apache.tomcat.JarScanType;
|
||||
import org.apache.tomcat.util.net.SSLHostConfig;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.awaitility.Awaitility;
|
||||
@@ -697,6 +698,33 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
|
||||
assertThat(verifier.getLastPrincipal()).isEqualTo("CN=2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "1.crt", "1.key", "2.crt", "2.key" })
|
||||
void shouldRetainSslHostConfigCustomizationsWhenReloadingSslBundles() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry("test",
|
||||
createPemSslBundle("classpath:1.crt", "classpath:1.key"));
|
||||
factory.setSslBundles(bundles);
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.addConnectorCustomizers((connector) -> {
|
||||
if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol<?> protocol) {
|
||||
for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) {
|
||||
sslHostConfig.setSessionTimeout(12345);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
bundles.updateBundle("test", createPemSslBundle("classpath:2.crt", "classpath:2.key"));
|
||||
Connector connector = ((TomcatWebServer) this.webServer).getTomcat().getConnector();
|
||||
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
|
||||
assertThat(protocol.findSslHostConfigs()).hasSize(1);
|
||||
SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0];
|
||||
assertThat(sslHostConfig.getSessionTimeout()).isEqualTo(12345);
|
||||
assertThat(sslHostConfig.getCertificates()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslWithHttp11Nio2Protocol() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user