Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,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).

Expand Down Expand Up @@ -2674,6 +2686,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*:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,32 @@
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;
import java.net.SocketException;
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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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());
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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
*/
Expand Down
Loading