Skip to content
Closed
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
53 changes: 53 additions & 0 deletions zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,18 @@ Both subsystems need to have sufficient amount of threads to achieve peak read t

#### AdminServer configuration

**New in 3.8.7:** [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.7.1:** The following
options are used to configure the [AdminServer](#sc_adminserver).

Expand Down Expand Up @@ -2554,6 +2566,47 @@ The AdminServer is enabled by default, but can be disabled by either:
Note that the TCP four-letter word interface is still available if
the AdminServer is disabled.

##### Configuring AdminServer for SSL/TLS
- Generating the **keystore.jks** and **truststore.jks** which can be found in the [Quorum TLS](#Quorum+TLS).
- Add the following configuration settings to the `zoo.cfg` config file:

```
admin.portUnification=true
ssl.quorum.keyStore.location=/path/to/keystore.jks
ssl.quorum.keyStore.password=password
ssl.quorum.trustStore.location=/path/to/truststore.jks
ssl.quorum.trustStore.password=password
```
- Verify that the following entries in the logs can be seen:

```
2019-08-03 15:44:55,213 [myid:] - INFO [main:JettyAdminServer@123] - Successfully loaded private key from /data/software/cert/keystore.jks
2019-08-03 15:44:55,213 [myid:] - INFO [main:JettyAdminServer@124] - Successfully loaded certificate authority from /data/software/cert/truststore.jks

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
```
Comment on lines +2569 to +2587
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section was already present in master, since this branch also supports AdminServer with TLS, so it is relevant and useful here, I added it here.


###### Restrict TLS protocols and cipher suites for SSL/TLS negotiation in AdminServer

From 3.8.7 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 @@ -149,6 +149,20 @@ public JettyAdminServer(
sslContextFactory.setTrustStore(trustStore);
sslContextFactory.setTrustStorePassword(certAuthPassword);

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 All @@ -64,6 +76,9 @@ public class JettyAdminServerTest extends ZKTestCase {
private static final String URL_FORMAT = "http://localhost:%d/commands";
private static final String HTTPS_URL_FORMAT = "https://localhost:%d/commands";
private static 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 @@ -86,6 +101,8 @@ public void setupEncryption() {
.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 @@ -149,6 +166,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 @@ -307,6 +326,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