diff --git a/conf/broker.conf b/conf/broker.conf index e0278e4d1f378..538e8fe4561db 100644 --- a/conf/broker.conf +++ b/conf/broker.conf @@ -37,6 +37,16 @@ webServicePort=8080 # Port to use to server HTTPS request - By default TLS is disabled webServicePortTls= +# Specify the tls protocols the broker's web service will use to negotiate during TLS handshake +# (a comma-separated list of protocol names). +# Examples:- [TLSv1.3, TLSv1.2] +webServiceTlsProtocols= + +# Specify the tls cipher the broker will use to negotiate during TLS Handshake +# (a comma-separated list of ciphers). +# Examples:- [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] +webServiceTlsCiphers= + # Hostname or IP address the service binds on, default is 0.0.0.0. bindAddress=0.0.0.0 diff --git a/conf/proxy.conf b/conf/proxy.conf index ed181a79e1888..be5b1525182ee 100644 --- a/conf/proxy.conf +++ b/conf/proxy.conf @@ -69,6 +69,16 @@ webServicePort=8080 # Port to use to server HTTPS request webServicePortTls= +# Specify the tls protocols the proxy's web service will use to negotiate during TLS handshake +# (a comma-separated list of protocol names). +# Examples:- [TLSv1.3, TLSv1.2] +webServiceTlsProtocols= + +# Specify the tls cipher the proxy will use to negotiate during TLS Handshake +# (a comma-separated list of ciphers). +# Examples:- [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] +webServiceTlsCiphers= + # Path for the file used to determine the rotation status for the proxy instance when responding # to service discovery health checks statusFilePath= diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java index 7bc98814f44d9..7dd1d626aadbb 100644 --- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java +++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java @@ -141,6 +141,20 @@ public class ServiceConfiguration implements PulsarConfiguration { ) private Optional webServicePortTls = Optional.empty(); + @FieldContext( + category = CATEGORY_TLS, + doc = "Specify the tls protocols the proxy's web service will use to negotiate during TLS Handshake.\n\n" + + "Example:- [TLSv1.3, TLSv1.2]" + ) + private Set webServiceTlsProtocols = new TreeSet<>(); + + @FieldContext( + category = CATEGORY_TLS, + doc = "Specify the tls cipher the proxy's web service will use to negotiate during TLS Handshake.\n\n" + + "Example:- [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]" + ) + private Set webServiceTlsCiphers = new TreeSet<>(); + @FieldContext( category = CATEGORY_SERVER, doc = "Hostname or IP address the service binds on" diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java index 4d9ff83e3f903..e0ac54f54e0de 100644 --- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java +++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java @@ -111,6 +111,8 @@ public WebService(PulsarService pulsar) throws PulsarServerException { config.getTlsTrustStore(), config.getTlsTrustStorePassword(), config.isTlsRequireTrustedClientCertOnConnect(), + config.getWebServiceTlsCiphers(), + config.getWebServiceTlsProtocols(), config.getTlsCertRefreshCheckDurationSec() ); } else { diff --git a/pulsar-common/pom.xml b/pulsar-common/pom.xml index 6a0942dd243ca..5f1ac23a77fad 100644 --- a/pulsar-common/pom.xml +++ b/pulsar-common/pom.xml @@ -163,6 +163,20 @@ true + + + org.eclipse.jetty + jetty-server + test + + + + org.bouncycastle + bc-fips + ${bouncycastlefips.version} + test + + org.lz4 lz4-java diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SslContextFactoryWithAutoRefresh.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefresh.java similarity index 57% rename from pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SslContextFactoryWithAutoRefresh.java rename to pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefresh.java index 0882a3a0cb131..a0b931f2e6a0b 100644 --- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/SslContextFactoryWithAutoRefresh.java +++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefresh.java @@ -18,30 +18,29 @@ */ package org.apache.pulsar.common.util.keystoretls; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.security.GeneralSecurityException; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; import org.eclipse.jetty.util.ssl.SslContextFactory; +import java.util.Set; + /** * SslContextFactoryWithAutoRefresh that create SSLContext for web server, and refresh in time. */ -public class SslContextFactoryWithAutoRefresh extends SslContextFactory { +public class JettySslContextFactoryWithAutoRefresh extends SslContextFactory.Server { private final NetSslContextBuilder sslCtxRefresher; - public SslContextFactoryWithAutoRefresh(String sslProviderString, - String keyStoreTypeString, - String keyStore, - String keyStorePassword, - boolean allowInsecureConnection, - String trustStoreTypeString, - String trustStore, - String trustStorePassword, - boolean requireTrustedClientCertOnConnect, - long certRefreshInSec) - throws SSLException, FileNotFoundException, GeneralSecurityException, IOException { + public JettySslContextFactoryWithAutoRefresh(String sslProviderString, + String keyStoreTypeString, + String keyStore, + String keyStorePassword, + boolean allowInsecureConnection, + String trustStoreTypeString, + String trustStore, + String trustStorePassword, + boolean requireTrustedClientCertOnConnect, + Set ciphers, + Set protocols, + long certRefreshInSec) { super(); sslCtxRefresher = new NetSslContextBuilder( sslProviderString, @@ -54,6 +53,12 @@ public SslContextFactoryWithAutoRefresh(String sslProviderString, trustStorePassword, requireTrustedClientCertOnConnect, certRefreshInSec); + if (ciphers != null && ciphers.size() > 0) { + this.setIncludeCipherSuites(ciphers.toArray(new String[0])); + } + if (protocols != null && protocols.size() > 0) { + this.setIncludeProtocols(protocols.toArray(new String[0])); + } if (sslProviderString != null) { setProvider(sslProviderString); } diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java index 987a32b216cbc..c455dcc761d33 100644 --- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java +++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/KeyStoreSSLContext.java @@ -256,7 +256,8 @@ public static KeyStoreSSLContext createServerKeyStoreSslContext(String sslProvid return keyStoreSSLContext; } - // for web server use case, no need ciphers and protocols + // the web server only use this method to get SSLContext, it won't use this to configure engine + // no need ciphers and protocols public static SSLContext createServerSslContext(String sslProviderString, String keyStoreTypeString, String keyStorePath, @@ -335,18 +336,19 @@ public static SSLContext createClientSslContext(String keyStoreTypeString, } // for web server. autoRefresh is default true. - public static SslContextFactory createSslContextFactory(String sslProviderString, - String keyStoreTypeString, - String keyStore, - String keyStorePassword, - boolean allowInsecureConnection, - String trustStoreTypeString, - String trustStore, - String trustStorePassword, - boolean requireTrustedClientCertOnConnect, - long certRefreshInSec) - throws GeneralSecurityException, IOException { - SslContextFactory sslCtxFactory; + public static SslContextFactory.Server createSslContextFactory(String sslProviderString, + String keyStoreTypeString, + String keyStore, + String keyStorePassword, + boolean allowInsecureConnection, + String trustStoreTypeString, + String trustStore, + String trustStorePassword, + boolean requireTrustedClientCertOnConnect, + Set ciphers, + Set protocols, + long certRefreshInSec) { + SslContextFactory.Server sslCtxFactory; if (sslProviderString == null) { Provider provider = SecurityUtility.CONSCRYPT_PROVIDER; @@ -355,7 +357,7 @@ public static SslContextFactory createSslContextFactory(String sslProviderString } } - sslCtxFactory = new SslContextFactoryWithAutoRefresh( + sslCtxFactory = new JettySslContextFactoryWithAutoRefresh( sslProviderString, keyStoreTypeString, keyStore, @@ -365,6 +367,8 @@ public static SslContextFactory createSslContextFactory(String sslProviderString trustStore, trustStorePassword, requireTrustedClientCertOnConnect, + ciphers, + protocols, certRefreshInSec); if (requireTrustedClientCertOnConnect) { diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/NetSslContextBuilder.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/NetSslContextBuilder.java index 38ebdb452691c..ebe7ec19be52e 100644 --- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/NetSslContextBuilder.java +++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/keystoretls/NetSslContextBuilder.java @@ -18,11 +18,9 @@ */ package org.apache.pulsar.common.util.keystoretls; -import java.io.FileNotFoundException; import java.io.IOException; import java.security.GeneralSecurityException; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; import org.apache.pulsar.common.util.FileModifiedTimeUpdater; import org.apache.pulsar.common.util.SslContextAutoRefreshBuilder; @@ -70,7 +68,7 @@ public NetSslContextBuilder(String sslProviderString, @Override public synchronized SSLContext update() - throws SSLException, FileNotFoundException, GeneralSecurityException, IOException { + throws GeneralSecurityException, IOException { this.sslContext = KeyStoreSSLContext.createServerSslContext(tlsProvider, tlsKeyStoreType, tlsKeyStore.getFileName(), tlsKeyStorePassword, tlsAllowInsecureConnection, diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefreshTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefreshTest.java new file mode 100644 index 0000000000000..414fbb2efd0ce --- /dev/null +++ b/pulsar-common/src/test/java/org/apache/pulsar/common/util/keystoretls/JettySslContextFactoryWithAutoRefreshTest.java @@ -0,0 +1,192 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.pulsar.common.util.keystoretls; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Configurator; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.testng.annotations.Test; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +@Slf4j +public class JettySslContextFactoryWithAutoRefreshTest { + + @Test + public void testJettyTlsServerTls() throws Exception { + Configurator.setRootLevel(Level.INFO); + Server server = new Server(); + List connectors = new ArrayList<>(); + SslContextFactory.Server factory = KeyStoreSSLContext.createSslContextFactory(null, + "JKS", Resources.getResource("ssl/jetty_server_key.jks").getPath(), + "jetty_server_pwd", false, "JKS", + Resources.getResource("ssl/jetty_server_trust.jks").getPath(), + "jetty_server_pwd", true, null, + null, 600); + factory.setHostnameVerifier((s, sslSession) -> true); + ServerConnector connector = new ServerConnector(server, factory); + connector.setPort(0); + connectors.add(connector); + server.setConnectors(connectors.toArray(new ServerConnector[0])); + server.start(); + // client connect + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + RegistryBuilder registryBuilder = RegistryBuilder.create(); + registryBuilder.register("https", new SSLConnectionSocketFactory(getClientSslContext(), new NoopHostnameVerifier())); + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); + httpClientBuilder.setConnectionManager(cm); + CloseableHttpClient httpClient = httpClientBuilder.build(); + HttpGet httpGet = new HttpGet("https://localhost:" + connector.getLocalPort()); + httpClient.execute(httpGet); + httpClient.close(); + server.stop(); + } + + @Test(expectedExceptions = SSLHandshakeException.class) + public void testJettyTlsServerInvalidTlsProtocol() throws Exception { + Configurator.setRootLevel(Level.INFO); + Server server = new Server(); + List connectors = new ArrayList<>(); + SslContextFactory.Server factory = KeyStoreSSLContext.createSslContextFactory(null, + "JKS", Resources.getResource("ssl/jetty_server_key.jks").getPath(), + "jetty_server_pwd", false, "JKS", + Resources.getResource("ssl/jetty_server_trust.jks").getPath(), + "jetty_server_pwd", true, null, + new HashSet() { + { + this.add("TLSv1.3"); + } + }, 600); + factory.setHostnameVerifier((s, sslSession) -> true); + ServerConnector connector = new ServerConnector(server, factory); + connector.setPort(0); + connectors.add(connector); + server.setConnectors(connectors.toArray(new ServerConnector[0])); + server.start(); + // client connect + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + RegistryBuilder registryBuilder = RegistryBuilder.create(); + registryBuilder.register("https", new SSLConnectionSocketFactory(getClientSslContext(), + new String[]{"TLSv1.2"}, null, new NoopHostnameVerifier())); + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); + httpClientBuilder.setConnectionManager(cm); + CloseableHttpClient httpClient = httpClientBuilder.build(); + HttpGet httpGet = new HttpGet("https://localhost:" + connector.getLocalPort()); + httpClient.execute(httpGet); + httpClient.close(); + server.stop(); + } + + @Test(expectedExceptions = SSLHandshakeException.class) + public void testJettyTlsServerInvalidCipher() throws Exception { + Configurator.setRootLevel(Level.INFO); + Server server = new Server(); + List connectors = new ArrayList<>(); + SslContextFactory.Server factory = KeyStoreSSLContext.createSslContextFactory(null, + "JKS", Resources.getResource("ssl/jetty_server_key.jks").getPath(), + "jetty_server_pwd", false, "JKS", + Resources.getResource("ssl/jetty_server_trust.jks").getPath(), + "jetty_server_pwd", true, new HashSet() { + { + this.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + } + }, + new HashSet() { + { + this.add("TLSv1.2"); + } + }, 600); + factory.setHostnameVerifier((s, sslSession) -> true); + ServerConnector connector = new ServerConnector(server, factory); + connector.setPort(0); + connectors.add(connector); + server.setConnectors(connectors.toArray(new ServerConnector[0])); + server.start(); + // client connect + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + RegistryBuilder registryBuilder = RegistryBuilder.create(); + registryBuilder.register("https", new SSLConnectionSocketFactory(getClientSslContext(), + new String[]{"TLSv1.2"}, new String[]{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, new NoopHostnameVerifier())); + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registryBuilder.build()); + httpClientBuilder.setConnectionManager(cm); + CloseableHttpClient httpClient = httpClientBuilder.build(); + HttpGet httpGet = new HttpGet("https://localhost:" + connector.getLocalPort()); + httpClient.execute(httpGet); + httpClient.close(); + server.stop(); + } + + private static SSLContext getClientSslContext() { + return getSslContext(Resources.getResource("ssl/jetty_client_key.jks").getPath(), + "jetty_client_pwd", + Resources.getResource("ssl/jetty_client_trust.jks").getPath(), + "jetty_client_pwd"); + } + + private static SSLContext getSslContext(String keyStorePath, String keyStorePassword, + String trustStorePath, String trustStorePassword) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + // key store + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (FileInputStream inputStream = new FileInputStream(keyStorePath)) { + keyStore.load(inputStream, keyStorePassword.toCharArray()); + } + keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()); + KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); + // trust store + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore trustStore = KeyStore.getInstance("JKS"); + try (FileInputStream inputStream = new FileInputStream(trustStorePath)) { + trustStore.load(inputStream, trustStorePassword.toCharArray()); + } + trustManagerFactory.init(trustStore); + sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), new SecureRandom()); + return sslContext; + } catch (Exception e) { + log.error("load ssl context error ", e); + return null; + } + } + +} \ No newline at end of file diff --git a/pulsar-common/src/test/resources/ssl/jetty_client_key.jks b/pulsar-common/src/test/resources/ssl/jetty_client_key.jks new file mode 100644 index 0000000000000..a969fb7fadc99 Binary files /dev/null and b/pulsar-common/src/test/resources/ssl/jetty_client_key.jks differ diff --git a/pulsar-common/src/test/resources/ssl/jetty_client_trust.jks b/pulsar-common/src/test/resources/ssl/jetty_client_trust.jks new file mode 100644 index 0000000000000..8e8cc29fa6620 Binary files /dev/null and b/pulsar-common/src/test/resources/ssl/jetty_client_trust.jks differ diff --git a/pulsar-common/src/test/resources/ssl/jetty_server_key.jks b/pulsar-common/src/test/resources/ssl/jetty_server_key.jks new file mode 100644 index 0000000000000..3ebc634d05f05 Binary files /dev/null and b/pulsar-common/src/test/resources/ssl/jetty_server_key.jks differ diff --git a/pulsar-common/src/test/resources/ssl/jetty_server_trust.jks b/pulsar-common/src/test/resources/ssl/jetty_server_trust.jks new file mode 100644 index 0000000000000..be7b5dea56757 Binary files /dev/null and b/pulsar-common/src/test/resources/ssl/jetty_server_trust.jks differ diff --git a/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/ProxyConfiguration.java b/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/ProxyConfiguration.java index bde85ea2c7a70..1d79357fcda8b 100644 --- a/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/ProxyConfiguration.java +++ b/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/ProxyConfiguration.java @@ -214,6 +214,20 @@ public class ProxyConfiguration implements PulsarConfiguration { ) private Optional webServicePortTls = Optional.empty(); + @FieldContext( + category = CATEGORY_TLS, + doc = "Specify the tls protocols the proxy's web service will use to negotiate during TLS Handshake.\n\n" + + "Example:- [TLSv1.3, TLSv1.2]" + ) + private Set webServiceTlsProtocols = new TreeSet<>(); + + @FieldContext( + category = CATEGORY_TLS, + doc = "Specify the tls cipher the proxy's web service will use to negotiate during TLS Handshake.\n\n" + + "Example:- [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]" + ) + private Set webServiceTlsCiphers = new TreeSet<>(); + @FieldContext( category = CATEGORY_SERVER, doc = "The directory where nar Extraction happens" diff --git a/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/WebServer.java b/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/WebServer.java index 62a2063b0b299..f874927cae388 100644 --- a/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/WebServer.java +++ b/pulsar-proxy/src/main/java/org/apache/pulsar/proxy/server/WebServer.java @@ -111,6 +111,8 @@ public WebServer(ProxyConfiguration config, AuthenticationService authentication config.getTlsTrustStore(), config.getTlsTrustStorePassword(), config.isTlsRequireTrustedClientCertOnConnect(), + config.getWebServiceTlsCiphers(), + config.getWebServiceTlsProtocols(), config.getTlsCertRefreshCheckDurationSec() ); } else {