浏览代码

ZOOKEEPER-4396: Read Key/trust store password from file

Key/trust store password is currently specified as plain text via system property or config property. To avoid exposing passwords as plain text and reduce security vulnerability, we provide the support of reading passwords from files.

The following four properties are added:

1. ssl.keyStore.passwordPath
2. ssl.quorum.keyStore.passwordPath
3. ssl.trustStore.passwordPath
4. ssl.quorum.trustStore.passwordPath

Specifies the file path that contains the key/trust store password. Reading the password from a file takes precedence over the explicit password property.

Author: liwang <liwang@apple.com>

Reviewers: hristopher Tubbs <ctubbsii@apache.org>, Enrico Olivelli <eolivelli@apache.org>, Mate Szalay-Beko <symat@apache.org>

Closes #1773 from li4wang/ZOOKEEPER-4396
liwang 3 年之前
父节点
当前提交
b2c1b5af36

+ 12 - 0
zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md

@@ -1642,6 +1642,12 @@ and [SASL authentication for ZooKeeper](https://cwiki.apache.org/confluence/disp
     Specifies the file path to a Java keystore containing the local
     credentials to be used for client and quorum TLS connections, and the
     password to unlock the file.
+    
+* *ssl.keyStore.passwordPath* and *ssl.quorum.keyStore.passwordPath* :
+    (Java system properties: **zookeeper.ssl.keyStore.passwordPath** and **zookeeper.ssl.quorum.keyStore.passwordPath**)
+    **New in 3.8.0:**
+    Specifies the file path that contains the keystore password. Reading the password from a file takes precedence over 
+    the explicit password property.
 
 * *ssl.keyStore.type* and *ssl.quorum.keyStore.type* :
     (Java system properties: **zookeeper.ssl.keyStore.type** and **zookeeper.ssl.quorum.keyStore.type**)
@@ -1658,6 +1664,12 @@ and [SASL authentication for ZooKeeper](https://cwiki.apache.org/confluence/disp
     credentials to be used for client and quorum TLS connections, and the
     password to unlock the file.
 
+* *ssl.trustStore.passwordPath* and *ssl.quorum.trustStore.passwordPath* :
+    (Java system properties: **zookeeper.ssl.trustStore.passwordPath** and **zookeeper.ssl.quorum.trustStore.passwordPath**)
+    **New in 3.8.0:**
+    Specifies the file path that contains the truststore password. Reading the password from a file takes precedence over 
+    the explicit password property.
+   
 * *ssl.trustStore.type* and *ssl.quorum.trustStore.type* :
     (Java system properties: **zookeeper.ssl.trustStore.type** and **zookeeper.ssl.quorum.trustStore.type**)
     **New in 3.5.5:**

+ 9 - 1
zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md

@@ -1341,11 +1341,19 @@ and [SASL authentication for ZooKeeper](https://cwiki.apache.org/confluence/disp
     **New in 3.5.5:**
     Specifies the file path to a JKS containing the local credentials to be used for SSL connections,
     and the password to unlock the file.
-
+    
+* *zookeeper.ssl.keyStore.passwordPath* :
+    **New in 3.8.0:**
+    Specifies the file path which contains the keystore password    
+    
 * *zookeeper.ssl.trustStore.location and zookeeper.ssl.trustStore.password* :
     **New in 3.5.5:**
     Specifies the file path to a JKS containing the remote credentials to be used for SSL connections,
     and the password to unlock the file.
+    
+* *zookeeper.ssl.trustStore.passwordPath* :
+    **New in 3.8.0:**
+    Specifies the file path which contains the truststore password       
 
 * *zookeeper.ssl.keyStore.type* and *zookeeper.ssl.trustStore.type*:
     **New in 3.5.5:**

+ 53 - 0
zookeeper-server/src/main/java/org/apache/zookeeper/common/SecretUtils.java

@@ -0,0 +1,53 @@
+/*
+ * 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.zookeeper.common;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for handling secret such as key/trust store password
+ */
+public final class SecretUtils {
+    private static final Logger LOG = LoggerFactory.getLogger(SecretUtils.class);
+
+    private SecretUtils() {
+    }
+
+    public static char[] readSecret(final String pathToFile) {
+        LOG.info("Reading secret from {}", pathToFile);
+
+        try {
+            final String secretValue = new String(
+                    Files.readAllBytes(Paths.get(pathToFile)), StandardCharsets.UTF_8);
+
+            if (secretValue.endsWith(System.lineSeparator())) {
+                return secretValue.substring(0, secretValue.length() - System.lineSeparator().length()).toCharArray();
+            }
+
+            return secretValue.toCharArray();
+        } catch (final Throwable e) {
+            LOG.error("Exception occurred when reading secret from file {}", pathToFile, e);
+            throw new IllegalStateException("Exception occurred when reading secret from file " + pathToFile, e);
+        }
+    }
+}

+ 32 - 2
zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java

@@ -148,9 +148,11 @@ public abstract class X509Util implements Closeable, AutoCloseable {
     private String cipherSuitesProperty = getConfigPrefix() + "ciphersuites";
     private String sslKeystoreLocationProperty = getConfigPrefix() + "keyStore.location";
     private String sslKeystorePasswdProperty = getConfigPrefix() + "keyStore.password";
+    private String sslKeystorePasswdPathProperty = getConfigPrefix() + "keyStore.passwordPath";
     private String sslKeystoreTypeProperty = getConfigPrefix() + "keyStore.type";
     private String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location";
     private String sslTruststorePasswdProperty = getConfigPrefix() + "trustStore.password";
+    private String sslTruststorePasswdPathProperty = getConfigPrefix() + "trustStore.passwordPath";
     private String sslTruststoreTypeProperty = getConfigPrefix() + "trustStore.type";
     private String sslContextSupplierClassProperty = getConfigPrefix() + "context.supplier.class";
     private String sslHostnameVerificationEnabledProperty = getConfigPrefix() + "hostnameVerification";
@@ -202,6 +204,10 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         return sslKeystorePasswdProperty;
     }
 
+    public String getSslKeystorePasswdPathProperty() {
+        return sslKeystorePasswdPathProperty;
+    }
+
     public String getSslKeystoreTypeProperty() {
         return sslKeystoreTypeProperty;
     }
@@ -214,6 +220,10 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         return sslTruststorePasswdProperty;
     }
 
+    public String getSslTruststorePasswdPathProperty() {
+        return sslTruststorePasswdPathProperty;
+    }
+
     public String getSslTruststoreTypeProperty() {
         return sslTruststoreTypeProperty;
     }
@@ -334,7 +344,7 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         TrustManager[] trustManagers = null;
 
         String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty, "");
-        String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty, "");
+        String keyStorePasswordProp = getPasswordFromConfigPropertyOrFile(config, sslKeystorePasswdProperty, sslKeystorePasswdPathProperty);
         String keyStoreTypeProp = config.getProperty(sslKeystoreTypeProperty);
 
         // There are legal states in some use cases for null KeyManager or TrustManager.
@@ -354,7 +364,7 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         }
 
         String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty, "");
-        String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty, "");
+        String trustStorePasswordProp = getPasswordFromConfigPropertyOrFile(config, sslTruststorePasswdProperty, sslTruststorePasswdPathProperty);
         String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty);
 
         boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty);
@@ -413,6 +423,26 @@ public abstract class X509Util implements Closeable, AutoCloseable {
             .loadTrustStore();
     }
 
+    /**
+     * Returns the password specified by the given property or from the file specified by the given path property.
+     * If both are specified, the value stored in the file will be returned.
+     *
+     * @param config  Zookeeper configuration
+     * @param propertyName  property name
+     * @param pathPropertyName path property name
+     * @return the password value
+     */
+    public String getPasswordFromConfigPropertyOrFile(final ZKConfig config,
+                                                      final String propertyName,
+                                                      final String pathPropertyName) {
+        String value = config.getProperty(propertyName, "");
+        final String pathProperty = config.getProperty(pathPropertyName, "");
+        if (!pathProperty.isEmpty()) {
+            value = String.valueOf(SecretUtils.readSecret(pathProperty));
+        }
+        return value;
+    }
+
     /**
      * Creates a key manager by loading the key store from the given file of
      * the given type, optionally decrypting it using the given password.

+ 2 - 0
zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java

@@ -118,9 +118,11 @@ public class ZKConfig {
         properties.put(x509Util.getSslCipherSuitesProperty(), System.getProperty(x509Util.getSslCipherSuitesProperty()));
         properties.put(x509Util.getSslKeystoreLocationProperty(), System.getProperty(x509Util.getSslKeystoreLocationProperty()));
         properties.put(x509Util.getSslKeystorePasswdProperty(), System.getProperty(x509Util.getSslKeystorePasswdProperty()));
+        properties.put(x509Util.getSslKeystorePasswdPathProperty(), System.getProperty(x509Util.getSslKeystorePasswdPathProperty()));
         properties.put(x509Util.getSslKeystoreTypeProperty(), System.getProperty(x509Util.getSslKeystoreTypeProperty()));
         properties.put(x509Util.getSslTruststoreLocationProperty(), System.getProperty(x509Util.getSslTruststoreLocationProperty()));
         properties.put(x509Util.getSslTruststorePasswdProperty(), System.getProperty(x509Util.getSslTruststorePasswdProperty()));
+        properties.put(x509Util.getSslTruststorePasswdPathProperty(), System.getProperty(x509Util.getSslTruststorePasswdPathProperty()));
         properties.put(x509Util.getSslTruststoreTypeProperty(), System.getProperty(x509Util.getSslTruststoreTypeProperty()));
         properties.put(x509Util.getSslContextSupplierClassProperty(), System.getProperty(x509Util.getSslContextSupplierClassProperty()));
         properties.put(x509Util.getSslHostnameVerificationEnabledProperty(), System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty()));

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

@@ -30,6 +30,7 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.zookeeper.common.QuorumX509Util;
+import org.apache.zookeeper.common.SecretUtils;
 import org.apache.zookeeper.common.X509Util;
 import org.apache.zookeeper.server.ZooKeeperServer;
 import org.eclipse.jetty.http.HttpVersion;
@@ -121,10 +122,15 @@ public class JettyAdminServer implements AdminServer {
             try (QuorumX509Util x509Util = new QuorumX509Util()) {
                 String privateKeyType = System.getProperty(x509Util.getSslKeystoreTypeProperty(), "");
                 String privateKeyPath = System.getProperty(x509Util.getSslKeystoreLocationProperty(), "");
-                String privateKeyPassword = System.getProperty(x509Util.getSslKeystorePasswdProperty(), "");
+                String privateKeyPassword = getPasswordFromSystemPropertyOrFile(
+                        x509Util.getSslKeystorePasswdProperty(),
+                        x509Util.getSslKeystorePasswdPathProperty());
+
                 String certAuthType = System.getProperty(x509Util.getSslTruststoreTypeProperty(), "");
                 String certAuthPath = System.getProperty(x509Util.getSslTruststoreLocationProperty(), "");
-                String certAuthPassword = System.getProperty(x509Util.getSslTruststorePasswdProperty(), "");
+                String certAuthPassword = getPasswordFromSystemPropertyOrFile(
+                        x509Util.getSslTruststorePasswdProperty(),
+                        x509Util.getSslTruststorePasswdPathProperty());
                 KeyStore keyStore = null, trustStore = null;
 
                 try {
@@ -289,4 +295,21 @@ public class JettyAdminServer implements AdminServer {
 
         ctxHandler.setSecurityHandler(securityHandler);
     }
+
+    /**
+     * Returns the password specified by the given property or stored in the file specified by the
+     * given path property. If both are specified, the password stored in the file will be returned.
+     * @param propertyName the name of the property
+     * @param pathPropertyName the name of the path property
+     * @return password value
+     */
+    private String getPasswordFromSystemPropertyOrFile(final String propertyName,
+                                                       final String pathPropertyName) {
+        String value = System.getProperty(propertyName, "");
+        final String pathValue = System.getProperty(pathPropertyName, "");
+        if (!pathValue.isEmpty()) {
+            value = String.valueOf(SecretUtils.readSecret(pathValue));
+        }
+        return value;
+    }
 }

+ 11 - 2
zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java

@@ -45,6 +45,9 @@ import org.slf4j.LoggerFactory;
  * <br>To specify store passwords, set the following system properties:
  * <br><code>zookeeper.ssl.keyStore.password</code>
  * <br><code>zookeeper.ssl.trustStore.password</code>
+ * <br>Alternatively, the passwords can be specified by the following password file path properties:
+ * <br><code>zookeeper.ssl.keyStore.passwordPath</code>
+ * <br><code>zookeeper.ssl.trustStore.passwordPath</code>
  * <br>Alternatively, this can be plugged with any X509TrustManager and
  * X509KeyManager implementation.
  */
@@ -61,13 +64,17 @@ public class X509AuthenticationProvider implements AuthenticationProvider {
      * <br><code>zookeeper.ssl.keyStore.location</code>
      * <br><code>zookeeper.ssl.trustStore.location</code>
      * <br><code>zookeeper.ssl.keyStore.password</code>
+     * <br><code>zookeeper.ssl.keyStore.passwordPath</code>
      * <br><code>zookeeper.ssl.trustStore.password</code>
+     * <br><code>zookeeper.ssl.trustStore.passwordPath</code>
      */
     public X509AuthenticationProvider() throws X509Exception {
         ZKConfig config = new ZKConfig();
         try (X509Util x509Util = new ClientX509Util()) {
             String keyStoreLocation = config.getProperty(x509Util.getSslKeystoreLocationProperty(), "");
-            String keyStorePassword = config.getProperty(x509Util.getSslKeystorePasswdProperty(), "");
+            String keyStorePassword = x509Util.getPasswordFromConfigPropertyOrFile(config,
+                    x509Util.getSslKeystorePasswdProperty(),
+                    x509Util.getSslKeystorePasswdPathProperty());
             String keyStoreTypeProp = config.getProperty(x509Util.getSslKeystoreTypeProperty());
 
             boolean crlEnabled = Boolean.parseBoolean(config.getProperty(x509Util.getSslCrlEnabledProperty()));
@@ -87,7 +94,9 @@ public class X509AuthenticationProvider implements AuthenticationProvider {
             }
 
             String trustStoreLocation = config.getProperty(x509Util.getSslTruststoreLocationProperty(), "");
-            String trustStorePassword = config.getProperty(x509Util.getSslTruststorePasswdProperty(), "");
+            String trustStorePassword = x509Util.getPasswordFromConfigPropertyOrFile(config,
+                    x509Util.getSslTruststorePasswdProperty(),
+                    x509Util.getSslTruststorePasswdPathProperty());
             String trustStoreTypeProp = config.getProperty(x509Util.getSslTruststoreTypeProperty());
 
             if (trustStoreLocation.isEmpty()) {

+ 3 - 1
zookeeper-server/src/test/java/org/apache/zookeeper/common/BaseX509ParameterizedTestCase.java

@@ -42,6 +42,8 @@ import org.junit.jupiter.params.provider.Arguments;
  * and caching the X509TestContext objects used by the tests.
  */
 public abstract class BaseX509ParameterizedTestCase extends ZKTestCase {
+    protected static final String KEY_NON_EMPTY_PASSWORD = "pa$$w0rd";
+    protected static final String KEY_EMPTY_PASSWORD = "";
 
     /**
      * Default parameters suitable for most subclasses. See example usage
@@ -53,7 +55,7 @@ public abstract class BaseX509ParameterizedTestCase extends ZKTestCase {
         int paramIndex = 0;
         for (X509KeyType caKeyType : X509KeyType.values()) {
             for (X509KeyType certKeyType : X509KeyType.values()) {
-                for (String keyPassword : new String[]{"", "pa$$w0rd"}) {
+                for (String keyPassword : new String[]{KEY_EMPTY_PASSWORD, KEY_NON_EMPTY_PASSWORD}) {
                     result.add(Arguments.of(caKeyType, certKeyType, keyPassword, paramIndex++));
                 }
             }

+ 70 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/common/SecretUtilsTest.java

@@ -0,0 +1,70 @@
+/*
+ * 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.zookeeper.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class SecretUtilsTest {
+
+    @ParameterizedTest
+    @ValueSource (strings = {"test secret", ""})
+    public void testReadSecret(final String secretTxt) throws Exception {
+        final Path secretFile = createSecretFile(secretTxt);
+
+        final char[] secret = SecretUtils.readSecret(secretFile.toString());
+        assertEquals(secretTxt, String.valueOf(secret));
+    }
+
+    @Test
+    public void tesReadSecret_withLineSeparator() throws Exception {
+        final String secretTxt = "test secret  with line separator" + System.lineSeparator();
+        final Path secretFile = createSecretFile(secretTxt);
+
+        final char[] secret = SecretUtils.readSecret(secretFile.toString());
+        assertEquals(secretTxt.substring(0, secretTxt.length() - 1), String.valueOf(secret));
+    }
+
+    @Test
+    public void testReadSecret_fileNotExist() {
+        final String pathToFile = "NonExistingFile";
+        final IllegalStateException exception =
+                assertThrows(IllegalStateException.class, () -> SecretUtils.readSecret(pathToFile));
+        assertEquals("Exception occurred while reading secret from file " + pathToFile, exception.getMessage());
+    }
+
+    public static Path createSecretFile(final String secretTxt) throws IOException {
+        final Path path = Files.createTempFile("test_", ".secrete");
+
+        final BufferedWriter writer = new BufferedWriter(new FileWriter(path.toString()));
+        writer.append(secretTxt);
+        writer.close();
+
+        path.toFile().deleteOnExit();
+        return path;
+    }
+}

+ 2 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java

@@ -381,9 +381,11 @@ public class X509TestContext {
     public void clearSystemProperties(X509Util x509Util) {
         System.clearProperty(x509Util.getSslKeystoreLocationProperty());
         System.clearProperty(x509Util.getSslKeystorePasswdProperty());
+        System.clearProperty(x509Util.getSslKeystorePasswdPathProperty());
         System.clearProperty(x509Util.getSslKeystoreTypeProperty());
         System.clearProperty(x509Util.getSslTruststoreLocationProperty());
         System.clearProperty(x509Util.getSslTruststorePasswdProperty());
+        System.clearProperty(x509Util.getSslTruststorePasswdPathProperty());
         System.clearProperty(x509Util.getSslTruststoreTypeProperty());
         System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty());
     }

+ 74 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java

@@ -28,6 +28,7 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
+import java.nio.file.Path;
 import java.security.NoSuchAlgorithmException;
 import java.security.Security;
 import java.util.concurrent.Callable;
@@ -143,6 +144,57 @@ public class X509UtilTest extends BaseX509ParameterizedTestCase {
         });
     }
 
+    @ParameterizedTest
+    @MethodSource("data")
+    @Timeout(value = 5)
+    public void testCreateSSLContext_withKeyStorePasswordFromFile(final X509KeyType caKeyType,
+                                                                 final X509KeyType certKeyType,
+                                                                 final String keyPassword,
+                                                                 final Integer paramIndex) throws Exception {
+        init(caKeyType, certKeyType, keyPassword, paramIndex);
+
+        testCreateSSLContext_withPasswordFromFile(keyPassword,
+                x509Util.getSslKeystorePasswdProperty(),
+                x509Util.getSslKeystorePasswdPathProperty());
+    }
+
+
+    @ParameterizedTest
+    @MethodSource("data")
+    @Timeout(value = 5)
+    public void testCreateSSLContext_withTrustStorePasswordFromFile(final X509KeyType caKeyType,
+                                                                   final X509KeyType certKeyType,
+                                                                   final String keyPassword,
+                                                                   final Integer paramIndex) throws Exception {
+        init(caKeyType, certKeyType, keyPassword, paramIndex);
+
+        testCreateSSLContext_withPasswordFromFile(keyPassword,
+                x509Util.getSslTruststorePasswdProperty(),
+                x509Util.getSslTruststorePasswdPathProperty());
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    @Timeout(value = 5)
+    public void testCreateSSLContext_withWrongKeyStorePasswordFromFile(final X509KeyType caKeyType,
+                                                                      final X509KeyType certKeyType,
+                                                                      final String keyPassword,
+                                                                      final Integer paramIndex) throws Exception {
+        init(caKeyType, certKeyType, keyPassword, paramIndex);
+        testCreateSSLContext_withWrongPasswordFromFile(keyPassword, x509Util.getSslKeystorePasswdPathProperty());
+    }
+
+    @ParameterizedTest
+    @MethodSource("data")
+    @Timeout(value = 5)
+    public void testCreateSSLContext_withWrongTrustStorePasswordFromFile(final X509KeyType caKeyType,
+                                                                         final X509KeyType certKeyType,
+                                                                         final String keyPassword,
+                                                                         final Integer paramIndex) throws Exception {
+        init(caKeyType, certKeyType, keyPassword, paramIndex);
+        testCreateSSLContext_withWrongPasswordFromFile(keyPassword, x509Util.getSslTruststorePasswdPathProperty());
+    }
+
     @ParameterizedTest
     @MethodSource("data")
     @Timeout(value = 5)
@@ -834,4 +886,26 @@ public class X509UtilTest extends BaseX509ParameterizedTestCase {
 
     }
 
+    private void testCreateSSLContext_withPasswordFromFile(final String keyPassword,
+                                                           final String propertyName,
+                                                           final String pathPropertyName) throws Exception {
+
+        final Path secretFile = SecretUtilsTest.createSecretFile(keyPassword);
+
+        System.clearProperty(propertyName);
+        System.setProperty(pathPropertyName, secretFile.toString());
+
+        x509Util.getDefaultSSLContext();
+    }
+
+    private void testCreateSSLContext_withWrongPasswordFromFile(final String keyPassword,
+                                                                final String pathPropertyName) throws Exception {
+
+        final Path secretFile = SecretUtilsTest.createSecretFile(keyPassword + "_wrong");
+
+        assertThrows(X509Exception.SSLContextException.class, () -> {
+            System.setProperty(pathPropertyName, secretFile.toString());
+            x509Util.getDefaultSSLContext();
+        });
+    }
 }

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

@@ -19,6 +19,7 @@
 package org.apache.zookeeper.server.admin;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import java.io.BufferedReader;
@@ -28,6 +29,7 @@ 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.Security;
 import java.security.cert.X509Certificate;
@@ -40,6 +42,7 @@ import javax.net.ssl.X509TrustManager;
 import org.apache.zookeeper.PortAssignment;
 import org.apache.zookeeper.ZKTestCase;
 import org.apache.zookeeper.common.KeyStoreFileType;
+import org.apache.zookeeper.common.SecretUtilsTest;
 import org.apache.zookeeper.common.X509Exception.SSLContextException;
 import org.apache.zookeeper.common.X509KeyType;
 import org.apache.zookeeper.common.X509TestContext;
@@ -140,9 +143,11 @@ public class JettyAdminServerTest extends ZKTestCase {
 
         System.clearProperty("zookeeper.ssl.quorum.keyStore.location");
         System.clearProperty("zookeeper.ssl.quorum.keyStore.password");
+        System.clearProperty("zookeeper.ssl.quorum.keyStore.passwordPath");
         System.clearProperty("zookeeper.ssl.quorum.keyStore.type");
         System.clearProperty("zookeeper.ssl.quorum.trustStore.location");
         System.clearProperty("zookeeper.ssl.quorum.trustStore.password");
+        System.clearProperty("zookeeper.ssl.quorum.trustStore.passwordPath");
         System.clearProperty("zookeeper.ssl.quorum.trustStore.type");
         System.clearProperty("zookeeper.admin.portUnification");
         System.clearProperty("zookeeper.admin.forceHttps");
@@ -247,6 +252,16 @@ public class JettyAdminServerTest extends ZKTestCase {
         testForceHttps(false);
     }
 
+    @Test
+    public void testForceHttps_withWrongPasswordFromFile() throws Exception {
+        final Path secretFile = SecretUtilsTest.createSecretFile("" + "wrong");
+
+        System.setProperty("zookeeper.ssl.quorum.keyStore.passwordPath", secretFile.toString());
+        System.setProperty("zookeeper.ssl.quorum.trustStore.passwordPath", secretFile.toString());
+
+        assertThrows(IOException.class, () -> testForceHttps(false));
+    }
+
     private void testForceHttps(boolean portUnification) throws Exception {
         System.setProperty("zookeeper.admin.forceHttps", "true");
         System.setProperty("zookeeper.admin.portUnification", String.valueOf(portUnification));

+ 27 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/QuorumSSLTest.java

@@ -35,6 +35,7 @@ import java.io.OutputStream;
 import java.math.BigInteger;
 import java.net.InetSocketAddress;
 import java.net.URLDecoder;
+import java.nio.file.Path;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
@@ -59,6 +60,7 @@ import javax.net.ssl.SSLServerSocketFactory;
 import org.apache.zookeeper.PortAssignment;
 import org.apache.zookeeper.client.ZKClientConfig;
 import org.apache.zookeeper.common.QuorumX509Util;
+import org.apache.zookeeper.common.SecretUtilsTest;
 import org.apache.zookeeper.server.ServerCnxnFactory;
 import org.apache.zookeeper.test.ClientBase;
 import org.bouncycastle.asn1.ocsp.OCSPResponse;
@@ -465,8 +467,10 @@ public class QuorumSSLTest extends QuorumPeerTestBase {
     private void clearSSLSystemProperties() {
         System.clearProperty(quorumX509Util.getSslKeystoreLocationProperty());
         System.clearProperty(quorumX509Util.getSslKeystorePasswdProperty());
+        System.clearProperty(quorumX509Util.getSslKeystorePasswdPathProperty());
         System.clearProperty(quorumX509Util.getSslTruststoreLocationProperty());
         System.clearProperty(quorumX509Util.getSslTruststorePasswdProperty());
+        System.clearProperty(quorumX509Util.getSslTruststorePasswdPathProperty());
         System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty());
         System.clearProperty(quorumX509Util.getSslOcspEnabledProperty());
         System.clearProperty(quorumX509Util.getSslCrlEnabledProperty());
@@ -495,6 +499,29 @@ public class QuorumSSLTest extends QuorumPeerTestBase {
         assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
     }
 
+    @Test
+    @Timeout(value = 5, unit = TimeUnit.MINUTES)
+    public void testQuorumSSL_withPasswordFromFile() throws Exception {
+        final Path secretFile = SecretUtilsTest.createSecretFile(String.valueOf(PASSWORD));
+
+        System.clearProperty(quorumX509Util.getSslKeystorePasswdProperty());
+        System.setProperty(quorumX509Util.getSslKeystorePasswdPathProperty(), secretFile.toString());
+
+        System.clearProperty(quorumX509Util.getSslTruststorePasswdProperty());
+        System.setProperty(quorumX509Util.getSslTruststorePasswdPathProperty(), secretFile.toString());
+
+        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
+        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);
+        q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
+
+        q1.start();
+        q2.start();
+        q3.start();
+
+        assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
+        assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));
+        assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
+    }
 
     @Test
     @Timeout(value = 5, unit = TimeUnit.MINUTES)

+ 17 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientSSLTest.java

@@ -27,12 +27,14 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.io.IOException;
+import java.nio.file.Path;
 import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.PortAssignment;
 import org.apache.zookeeper.ZooDefs;
 import org.apache.zookeeper.ZooKeeper;
 import org.apache.zookeeper.client.ZKClientConfig;
 import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.common.SecretUtilsTest;
 import org.apache.zookeeper.server.NettyServerCnxnFactory;
 import org.apache.zookeeper.server.ServerCnxnFactory;
 import org.apache.zookeeper.server.auth.ProviderRegistry;
@@ -67,8 +69,10 @@ public class ClientSSLTest extends QuorumPeerTestBase {
         System.clearProperty(ZKClientConfig.SECURE_CLIENT);
         System.clearProperty(clientX509Util.getSslKeystoreLocationProperty());
         System.clearProperty(clientX509Util.getSslKeystorePasswdProperty());
+        System.clearProperty(clientX509Util.getSslKeystorePasswdPathProperty());
         System.clearProperty(clientX509Util.getSslTruststoreLocationProperty());
         System.clearProperty(clientX509Util.getSslTruststorePasswdProperty());
+        System.clearProperty(clientX509Util.getSslTruststorePasswdPathProperty());
         clientX509Util.close();
     }
 
@@ -110,6 +114,19 @@ public class ClientSSLTest extends QuorumPeerTestBase {
         testClientServerSSL(true);
     }
 
+    @Test
+    public void testClientServerSSL_withPasswordFromFile() throws Exception {
+        final Path secretFile = SecretUtilsTest.createSecretFile("testpass");
+
+        System.clearProperty(clientX509Util.getSslKeystorePasswdProperty());
+        System.setProperty(clientX509Util.getSslKeystorePasswdPathProperty(), secretFile.toString());
+
+        System.clearProperty(clientX509Util.getSslTruststorePasswdProperty());
+        System.setProperty(clientX509Util.getSslTruststorePasswdPathProperty(), secretFile.toString());
+
+        testClientServerSSL(true);
+    }
+
     public void testClientServerSSL(boolean useSecurePort) throws Exception {
         final int SERVER_COUNT = 3;
         final int[] clientPorts = new int[SERVER_COUNT];