Skip to content

Commit 49df972

Browse files
PDavidanmolnar
authored andcommitted
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 eab1659) Signed-off-by: Andor Molnar <andor@cloudera.com>
1 parent 1c74731 commit 49df972

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,18 @@ Both subsystems need to have sufficient amount of threads to achieve peak read t
21672167

21682168
#### AdminServer configuration
21692169

2170+
**New in 3.10.0:** [AdminServer](#sc_adminserver) will use the following existing properties:
2171+
2172+
* *ssl.quorum.ciphersuites* :
2173+
(Java system property: **zookeeper.ssl.quorum.ciphersuites**)
2174+
The enabled cipher suites to be used in TLS negotiation for AdminServer.
2175+
Default: Jetty default.
2176+
2177+
* *ssl.quorum.enabledProtocols* :
2178+
(Java system property: **zookeeper.ssl.quorum.enabledProtocols**)
2179+
The enabled protocols to be used in TLS negotiation for AdminServer.
2180+
Default: Jetty default.
2181+
21702182
**New in 3.9.0:** The following
21712183
options are used to configure the [AdminServer](#sc_adminserver).
21722184

@@ -2653,6 +2665,27 @@ ssl.quorum.trustStore.password=password
26532665
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
26542666
```
26552667

2668+
###### Restrict TLS protocols and cipher suites for SSL/TLS negotiation in AdminServer
2669+
2670+
From 3.10.0 AdminServer uses the following already existing properties:
2671+
2672+
* **ssl.quorum.enabledProtocols** to specify the enabled protocols,
2673+
* **ssl.quorum.ciphersuites** to specify the enabled cipher suites.
2674+
2675+
Add the following configuration settings to the `zoo.cfg` config file:
2676+
2677+
```
2678+
ssl.quorum.enabledProtocols=TLSv1.2,TLSv1.3
2679+
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
2680+
```
2681+
2682+
To verify raise the log level of JettyAdminServer to DEBUG and check that the following entries can be seen in the logs:
2683+
2684+
```
2685+
2026-03-11 11:38:01,102 [myid:] - DEBUG [main:o.a.z.s.a.JettyAdminServer@159] - Setting enabled protocols: 'TLSv1.2,TLSv1.3'
2686+
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'
2687+
```
2688+
26562689
Available commands include:
26572690

26582691
* *connection_stat_reset/crst*:

zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ public JettyAdminServer(
154154
sslContextFactory.setTrustStorePassword(certAuthPassword);
155155
sslContextFactory.setNeedClientAuth(needClientAuth);
156156

157+
String enabledProtocols = System.getProperty(x509Util.getSslEnabledProtocolsProperty());
158+
if (enabledProtocols != null) {
159+
LOG.debug("Setting enabled protocols: '{}'", enabledProtocols);
160+
String[] enabledProtocolsArray = enabledProtocols.split(",");
161+
sslContextFactory.setIncludeProtocols(enabledProtocolsArray);
162+
}
163+
164+
String sslCipherSuites = System.getProperty(x509Util.getSslCipherSuitesProperty());
165+
if (sslCipherSuites != null) {
166+
LOG.debug("Setting enabled cipherSuites: '{}'", sslCipherSuites);
167+
String[] cipherSuitesArray = sslCipherSuites.split(",");
168+
sslContextFactory.setIncludeCipherSuites(cipherSuitesArray);
169+
}
170+
157171
if (forceHttps) {
158172
connector = new ServerConnector(server,
159173
new SslConnectionFactory(sslContextFactory, HttpVersion.fromVersion(httpVersion).asString()),

zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,32 @@
2424
import static org.junit.jupiter.api.Assertions.fail;
2525
import java.io.BufferedReader;
2626
import java.io.File;
27+
import java.io.FileInputStream;
2728
import java.io.IOException;
2829
import java.io.InputStreamReader;
2930
import java.net.HttpURLConnection;
3031
import java.net.SocketException;
3132
import java.net.URL;
3233
import java.nio.file.Path;
3334
import java.security.GeneralSecurityException;
35+
import java.security.KeyStore;
36+
import java.security.KeyStoreException;
37+
import java.security.NoSuchAlgorithmException;
3438
import java.security.Security;
39+
import java.security.UnrecoverableKeyException;
40+
import java.security.cert.CertificateException;
3541
import java.security.cert.X509Certificate;
3642
import javax.net.ssl.HostnameVerifier;
3743
import javax.net.ssl.HttpsURLConnection;
44+
import javax.net.ssl.KeyManager;
45+
import javax.net.ssl.KeyManagerFactory;
3846
import javax.net.ssl.SSLContext;
47+
import javax.net.ssl.SSLHandshakeException;
3948
import javax.net.ssl.SSLSession;
49+
import javax.net.ssl.SSLSocket;
50+
import javax.net.ssl.SSLSocketFactory;
4051
import javax.net.ssl.TrustManager;
52+
import javax.net.ssl.TrustManagerFactory;
4153
import javax.net.ssl.X509TrustManager;
4254
import org.apache.zookeeper.PortAssignment;
4355
import org.apache.zookeeper.ZKTestCase;
@@ -65,6 +77,9 @@ public class JettyAdminServerTest extends ZKTestCase {
6577
static final String URL_FORMAT = "http://localhost:%d/commands";
6678
static final String HTTPS_URL_FORMAT = "https://localhost:%d/commands";
6779
private final int jettyAdminPort = PortAssignment.unique();
80+
private static final String KEYSTORE_TYPE_JKS = "JKS";
81+
private String keyStorePath;
82+
private String trustStorePath;
6883

6984
@BeforeEach
7085
public void enableServer() {
@@ -85,6 +100,8 @@ public void setupEncryption(@TempDir File tempDir) {
85100
.setTrustStorePassword("")
86101
.setTrustStoreKeyType(X509KeyType.EC)
87102
.build();
103+
keyStorePath = x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath();
104+
trustStorePath = x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath();
88105
System.setProperty(
89106
"zookeeper.ssl.quorum.keyStore.location",
90107
x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath());
@@ -148,6 +165,8 @@ public void cleanUp() {
148165
System.clearProperty("zookeeper.ssl.quorum.trustStore.password");
149166
System.clearProperty("zookeeper.ssl.quorum.trustStore.passwordPath");
150167
System.clearProperty("zookeeper.ssl.quorum.trustStore.type");
168+
System.clearProperty("zookeeper.ssl.quorum.ciphersuites");
169+
System.clearProperty("zookeeper.ssl.quorum.enabledProtocols");
151170
System.clearProperty("zookeeper.admin.portUnification");
152171
System.clearProperty("zookeeper.admin.forceHttps");
153172
}
@@ -306,6 +325,116 @@ private void queryAdminServer(String urlStr, boolean encrypted) throws IOExcepti
306325
assertTrue(line.length() > 0);
307326
}
308327

328+
@Test
329+
public void testHandshakeWithSupportedProtocol() throws Exception {
330+
System.setProperty("zookeeper.admin.forceHttps", "true");
331+
System.setProperty("zookeeper.ssl.quorum.enabledProtocols", "TLSv1.3");
332+
333+
JettyAdminServer server = new JettyAdminServer();
334+
try {
335+
server.start();
336+
337+
// Use a raw SSLSocket to verify the handshake
338+
SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.3");
339+
SSLSocketFactory factory = sslContext.getSocketFactory();
340+
341+
try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) {
342+
socket.startHandshake();
343+
String negotiatedProtocol = socket.getSession().getProtocol();
344+
345+
// Verify that we actually landed on the protocol we expected
346+
assertEquals("TLSv1.3", negotiatedProtocol,
347+
"The negotiated protocol should be TLSv1.3.");
348+
}
349+
} finally {
350+
server.shutdown();
351+
}
352+
}
353+
354+
@Test
355+
public void testHandshakeWithUnsupportedProtocolFails() throws Exception {
356+
System.setProperty("zookeeper.admin.forceHttps", "true");
357+
System.setProperty("zookeeper.ssl.quorum.enabledProtocols", "TLSv1.3");
358+
359+
JettyAdminServer server = new JettyAdminServer();
360+
try {
361+
server.start();
362+
363+
SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.1");
364+
SSLSocketFactory factory = sslContext.getSocketFactory();
365+
366+
try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) {
367+
SSLHandshakeException exception = assertThrows(SSLHandshakeException.class, socket::startHandshake);
368+
assertEquals(
369+
"No appropriate protocol (protocol is disabled or cipher suites are inappropriate)",
370+
exception.getMessage(),
371+
"The handshake should have failed due to a protocol mismatch.");
372+
}
373+
} finally {
374+
server.shutdown();
375+
}
376+
}
377+
378+
@Test
379+
public void testCipherMismatchFails() throws Exception {
380+
System.setProperty("zookeeper.admin.forceHttps", "true");
381+
System.setProperty("zookeeper.ssl.quorum.ciphersuites", "TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384");
382+
383+
JettyAdminServer server = new JettyAdminServer();
384+
try {
385+
server.start();
386+
387+
SSLContext sslContext = createSSLContext(keyStorePath, "".toCharArray(), trustStorePath, "TLSv1.2");
388+
SSLSocketFactory factory = sslContext.getSocketFactory();
389+
390+
try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", jettyAdminPort)) {
391+
// Force the client to use a cipher NOT enabled for the AdminServer
392+
String[] unsupportedCiphers = new String[]{"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"};
393+
socket.setEnabledCipherSuites(unsupportedCiphers);
394+
395+
assertThrows(SSLHandshakeException.class, socket::startHandshake,
396+
"The handshake should have failed due to a cipher mismatch.");
397+
}
398+
} finally {
399+
server.shutdown();
400+
}
401+
}
402+
403+
private SSLContext createSSLContext(String keystorePath, char[] password, String trustStorePath, String protocol)
404+
throws Exception {
405+
KeyManager[] keyManagers = getKeyManagers(keystorePath, password);
406+
TrustManager[] trustManagers = getTrustManagers(trustStorePath, password);
407+
408+
SSLContext sslContext = SSLContext.getInstance(protocol);
409+
sslContext.init(keyManagers, trustManagers, null);
410+
411+
return sslContext;
412+
}
413+
414+
private static KeyManager[] getKeyManagers(String keystorePath, char[] password) throws KeyStoreException,
415+
IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
416+
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE_JKS);
417+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
418+
keyStore.load(fis, password);
419+
}
420+
421+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
422+
kmf.init(keyStore, password);
423+
return kmf.getKeyManagers();
424+
}
425+
426+
public TrustManager[] getTrustManagers(String trustStorePath, char[] password) throws Exception {
427+
KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE_JKS);
428+
try (FileInputStream fis = new FileInputStream(trustStorePath)) {
429+
trustStore.load(fis, password);
430+
}
431+
432+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
433+
tmf.init(trustStore);
434+
435+
return tmf.getTrustManagers();
436+
}
437+
309438
/**
310439
* Using TRACE method to visit admin server
311440
*/

0 commit comments

Comments
 (0)