From 34fbde692b3f0f74352db84dcdc2a143dbcf7e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Paksy?= Date: Mon, 23 Mar 2026 17:41:49 +0100 Subject: [PATCH] ZOOKEEPER-5023: Allow to set TLS version and ciphers for AdminServer Reviewers: meszibalu, anmolnar Author: PDavid Closes #2359 from PDavid/ZOOKEEPER-5023-AdminServer-TLS-proto-ciphers (cherry picked from commit eab1659958f961bdb5b1538c91a0b4a49f69d5bc) --- .../main/resources/markdown/zookeeperAdmin.md | 33 +++++ .../server/admin/JettyAdminServer.java | 14 ++ .../server/admin/JettyAdminServerTest.java | 129 ++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md index 7b104257f3f..c9cc934b703 100644 --- a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md +++ b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md @@ -2167,6 +2167,18 @@ Both subsystems need to have sufficient amount of threads to achieve peak read t #### AdminServer configuration +**New in 3.10.0:** [AdminServer](#sc_adminserver) will use the following existing properties: + +* *ssl.quorum.ciphersuites* : + (Java system property: **zookeeper.ssl.quorum.ciphersuites**) + The enabled cipher suites to be used in TLS negotiation for AdminServer. + Default: Jetty default. + +* *ssl.quorum.enabledProtocols* : + (Java system property: **zookeeper.ssl.quorum.enabledProtocols**) + The enabled protocols to be used in TLS negotiation for AdminServer. + Default: Jetty default. + **New in 3.9.0:** The following options are used to configure the [AdminServer](#sc_adminserver). @@ -2653,6 +2665,27 @@ ssl.quorum.trustStore.password=password 2019-08-03 15:44:55,403 [myid:] - INFO [main:JettyAdminServer@170] - Started AdminServer on address 0.0.0.0, port 8080 and command URL /commands ``` +###### Restrict TLS protocols and cipher suites for SSL/TLS negotiation in AdminServer + +From 3.10.0 AdminServer uses the following already existing properties: + +* **ssl.quorum.enabledProtocols** to specify the enabled protocols, +* **ssl.quorum.ciphersuites** to specify the enabled cipher suites. + +Add the following configuration settings to the `zoo.cfg` config file: + +``` +ssl.quorum.enabledProtocols=TLSv1.2,TLSv1.3 +ssl.quorum.ciphersuites=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +``` + +To verify raise the log level of JettyAdminServer to DEBUG and check that the following entries can be seen in the logs: + +``` +2026-03-11 11:38:01,102 [myid:] - DEBUG [main:o.a.z.s.a.JettyAdminServer@159] - Setting enabled protocols: 'TLSv1.2,TLSv1.3' +2026-03-11 11:38:01,102 [myid:] - DEBUG [main:o.a.z.s.a.JettyAdminServer@166] - Setting enabled cipherSuites: 'TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' +``` + Available commands include: * *connection_stat_reset/crst*: diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java index a237e4c3b5f..3ec6f25b92e 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java @@ -154,6 +154,20 @@ public JettyAdminServer( sslContextFactory.setTrustStorePassword(certAuthPassword); sslContextFactory.setNeedClientAuth(needClientAuth); + String enabledProtocols = System.getProperty(x509Util.getSslEnabledProtocolsProperty()); + if (enabledProtocols != null) { + LOG.debug("Setting enabled protocols: '{}'", enabledProtocols); + String[] enabledProtocolsArray = enabledProtocols.split(","); + sslContextFactory.setIncludeProtocols(enabledProtocolsArray); + } + + String sslCipherSuites = System.getProperty(x509Util.getSslCipherSuitesProperty()); + if (sslCipherSuites != null) { + LOG.debug("Setting enabled cipherSuites: '{}'", sslCipherSuites); + String[] cipherSuitesArray = sslCipherSuites.split(","); + sslContextFactory.setIncludeCipherSuites(cipherSuitesArray); + } + if (forceHttps) { connector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.fromVersion(httpVersion).asString()), diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java index 8ef7e8b5f96..2123e515b48 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; @@ -31,13 +32,24 @@ import java.net.URL; import java.nio.file.Path; import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.apache.zookeeper.PortAssignment; import org.apache.zookeeper.ZKTestCase; @@ -65,6 +77,9 @@ public class JettyAdminServerTest extends ZKTestCase { static final String URL_FORMAT = "http://localhost:%d/commands"; static final String HTTPS_URL_FORMAT = "https://localhost:%d/commands"; private final int jettyAdminPort = PortAssignment.unique(); + private static final String KEYSTORE_TYPE_JKS = "JKS"; + private String keyStorePath; + private String trustStorePath; @BeforeEach public void enableServer() { @@ -85,6 +100,8 @@ public void setupEncryption(@TempDir File tempDir) { .setTrustStorePassword("") .setTrustStoreKeyType(X509KeyType.EC) .build(); + keyStorePath = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(); + trustStorePath = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(); System.setProperty( "zookeeper.ssl.quorum.keyStore.location", x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath()); @@ -148,6 +165,8 @@ public void cleanUp() { System.clearProperty("zookeeper.ssl.quorum.trustStore.password"); System.clearProperty("zookeeper.ssl.quorum.trustStore.passwordPath"); System.clearProperty("zookeeper.ssl.quorum.trustStore.type"); + System.clearProperty("zookeeper.ssl.quorum.ciphersuites"); + System.clearProperty("zookeeper.ssl.quorum.enabledProtocols"); System.clearProperty("zookeeper.admin.portUnification"); System.clearProperty("zookeeper.admin.forceHttps"); } @@ -306,6 +325,116 @@ private void queryAdminServer(String urlStr, boolean encrypted) throws IOExcepti assertTrue(line.length() > 0); } + @Test + public void testHandshakeWithSupportedProtocol() throws Exception { + System.setProperty("zookeeper.admin.forceHttps", "true"); + System.setProperty("zookeeper.ssl.quorum.enabledProtocols", "TLSv1.3"); + + JettyAdminServer server = new JettyAdminServer(); + try { + server.start(); + + // Use a raw SSLSocket to verify the handshake + SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.3"); + SSLSocketFactory factory = sslContext.getSocketFactory(); + + try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) { + socket.startHandshake(); + String negotiatedProtocol = socket.getSession().getProtocol(); + + // Verify that we actually landed on the protocol we expected + assertEquals("TLSv1.3", negotiatedProtocol, + "The negotiated protocol should be TLSv1.3."); + } + } finally { + server.shutdown(); + } + } + + @Test + public void testHandshakeWithUnsupportedProtocolFails() throws Exception { + System.setProperty("zookeeper.admin.forceHttps", "true"); + System.setProperty("zookeeper.ssl.quorum.enabledProtocols", "TLSv1.3"); + + JettyAdminServer server = new JettyAdminServer(); + try { + server.start(); + + SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.1"); + SSLSocketFactory factory = sslContext.getSocketFactory(); + + try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) { + SSLHandshakeException exception = assertThrows(SSLHandshakeException.class, socket::startHandshake); + assertEquals( + "No appropriate protocol (protocol is disabled or cipher suites are inappropriate)", + exception.getMessage(), + "The handshake should have failed due to a protocol mismatch."); + } + } finally { + server.shutdown(); + } + } + + @Test + public void testCipherMismatchFails() throws Exception { + System.setProperty("zookeeper.admin.forceHttps", "true"); + System.setProperty("zookeeper.ssl.quorum.ciphersuites", "TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384"); + + JettyAdminServer server = new JettyAdminServer(); + try { + server.start(); + + SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.2"); + SSLSocketFactory factory = sslContext.getSocketFactory(); + + try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) { + // Force the client to use a cipher NOT enabled for the AdminServer + String[] unsupportedCiphers = new String[]{"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"}; + socket.setEnabledCipherSuites(unsupportedCiphers); + + assertThrows(SSLHandshakeException.class, socket::startHandshake, + "The handshake should have failed due to a cipher mismatch."); + } + } finally { + server.shutdown(); + } + } + + private SSLContext createSSLContext(String keystorePath, char[] password, String trustStorePath, String protocol) + throws Exception { + KeyManager[] keyManagers = getKeyManagers(keystorePath, password); + TrustManager[] trustManagers = getTrustManagers(trustStorePath, password); + + SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(keyManagers, trustManagers, null); + + return sslContext; + } + + private static KeyManager[] getKeyManagers(String keystorePath, char[] password) throws KeyStoreException, + IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE_JKS); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + keyStore.load(fis, password); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, password); + return kmf.getKeyManagers(); + } + + public TrustManager[] getTrustManagers(String trustStorePath, char[] password) throws Exception { + KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE_JKS); + try (FileInputStream fis = new FileInputStream(trustStorePath)) { + trustStore.load(fis, password); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + + return tmf.getTrustManagers(); + } + /** * Using TRACE method to visit admin server */