You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zookeeper.apache.org by eo...@apache.org on 2022/03/01 13:31:03 UTC

[zookeeper] branch branch-3.7 updated: ZOOKEEPER-4477: Single Kerberos ticket renewal failure can prevent all future renewals since Java 9

This is an automated email from the ASF dual-hosted git repository.

eolivelli pushed a commit to branch branch-3.7
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/branch-3.7 by this push:
     new d9c1338  ZOOKEEPER-4477: Single Kerberos ticket renewal failure can prevent all future renewals since Java 9
d9c1338 is described below

commit d9c1338df1196ae515a692f895bae85dacdb3ac5
Author: Mate Szalay-Beko <sy...@apache.org>
AuthorDate: Tue Mar 1 14:30:23 2022 +0100

    ZOOKEEPER-4477: Single Kerberos ticket renewal failure can prevent all future renewals since Java 9
    
    This bug is similar to the one fixed in https://issues.apache.org/jira/browse/KAFKA-12730.
    
    Our Kerberos ticket refresh thread performs re-login by logging out and then logging in again. If
    login fails, we retry after some sleep. Every reLogin() operation performs loginContext.logout()
    and loginContext.login(). If login fails, we end up with two consecutive logouts. This used to
    work in older Java versions, but from Java 9 onwards, this results in a NullPointerException due
    to https://bugs.openjdk.java.net/browse/JDK-8173069. We should check if logout is required before
    attempting logout.
    
    I fixed the issue and added a new unit test to test some ticket renewal scenarios. I managed to
    reproduce the problem in KerberosTicketRenewalTest.shouldRecoverIfKerberosNotAvailableForSomeTime()
    which (before the fix) failed with Java13 but succeeded with Java8.
    
    Author: Mate Szalay-Beko <sy...@apache.org>
    
    Reviewers: Enrico Olivelli <eo...@apache.org>
    
    Closes #1828 from symat/ZOOKEEPER-4477-master
    
    (cherry picked from commit a5b6c38edd74b263638817f7b65d057c91d5a888)
    Signed-off-by: Enrico Olivelli <eo...@apache.org>
---
 .../src/main/java/org/apache/zookeeper/Login.java  |  31 ++-
 .../zookeeper/KerberosTicketRenewalTest.java       | 278 +++++++++++++++++++++
 .../server/quorum/auth/KerberosTestUtils.java      |  61 +++++
 .../zookeeper/server/quorum/auth/MiniKdc.java      |   9 +-
 .../zookeeper/server/quorum/auth/MiniKdcTest.java  |  59 +----
 5 files changed, 372 insertions(+), 66 deletions(-)

diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/Login.java b/zookeeper-server/src/main/java/org/apache/zookeeper/Login.java
index e74cddb..ab2bf63 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/Login.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/Login.java
@@ -63,7 +63,10 @@ public class Login {
     // Regardless of TICKET_RENEW_WINDOW setting above and the ticket expiry time,
     // thread will not sleep between refresh attempts any less than 1 minute (60*1000 milliseconds = 1 minute).
     // Change the '1' to e.g. 5, to change this to 5 minutes.
-    private static final long MIN_TIME_BEFORE_RELOGIN = 1 * 60 * 1000L;
+    private static final long DEFAULT_MIN_TIME_BEFORE_RELOGIN = 1 * 60 * 1000L;
+    public static final String MIN_TIME_BEFORE_RELOGIN_CONFIG_KEY = "zookeeper.kerberos.minReLoginTimeMs";
+    private static final long MIN_TIME_BEFORE_RELOGIN = Long.getLong(
+      MIN_TIME_BEFORE_RELOGIN_CONFIG_KEY, DEFAULT_MIN_TIME_BEFORE_RELOGIN);
 
     private Subject subject = null;
     private Thread t = null;
@@ -223,7 +226,7 @@ public class Login {
                                     --retry;
                                     // sleep for 10 seconds
                                     try {
-                                        Thread.sleep(10 * 1000);
+                                        sleepBeforeRetryFailedRefresh();
                                     } catch (InterruptedException ie) {
                                         LOG.error("Interrupted while renewing TGT, exiting Login thread");
                                         return;
@@ -251,7 +254,7 @@ public class Login {
                                     --retry;
                                     // sleep for 10 seconds.
                                     try {
-                                        Thread.sleep(10 * 1000);
+                                        sleepBeforeRetryFailedRefresh();
                                     } catch (InterruptedException e) {
                                         LOG.error("Interrupted during login retry after LoginException:", le);
                                         throw le;
@@ -400,10 +403,10 @@ public class Login {
     }
 
     /**
-     * Get the time of the last login.
-     * @return the number of milliseconds since the beginning of time.
+     * Get the time of the last login (ticket initialization or last ticket renewal).
+     * @return the number of milliseconds since epoch.
      */
-    private long getLastLogin() {
+    public long getLastLogin() {
         return lastLogin;
     }
 
@@ -428,7 +431,7 @@ public class Login {
             //clear up the kerberos state. But the tokens are not cleared! As per
             //the Java kerberos login module code, only the kerberos credentials
             //are cleared
-            login.logout();
+            logout();
             //login and also update the subject field of this instance to
             //have the new credentials (pass it to the LoginContext constructor)
             login = new LoginContext(loginContextName, getSubject());
@@ -438,4 +441,18 @@ public class Login {
         }
     }
 
+    // this method also visible for unit tests, to make sure kerberos state cleaned up
+    protected synchronized void logout() throws LoginException {
+        // We need to make sure not to call LoginContext.logout() when we
+        // are not logged in. Since Java 9 this could result in an NPE.
+        // See ZOOKEEPER-4477 for more details.
+        if (subject != null && !subject.getPrincipals().isEmpty()) {
+            login.logout();
+        }
+    }
+
+    // this method is overwritten in unit tests to test concurrency
+    protected void sleepBeforeRetryFailedRefresh() throws InterruptedException {
+        Thread.sleep(10 * 1000);
+    }
 }
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/KerberosTicketRenewalTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/KerberosTicketRenewalTest.java
new file mode 100644
index 0000000..84b2140
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/KerberosTicketRenewalTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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;
+
+import static org.apache.zookeeper.server.quorum.auth.MiniKdc.MAX_TICKET_LIFETIME;
+import static org.apache.zookeeper.server.quorum.auth.MiniKdc.MIN_TICKET_LIFETIME;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTimeout;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.security.Principal;
+import java.time.Duration;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginException;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.zookeeper.common.ZKConfig;
+import org.apache.zookeeper.server.quorum.auth.KerberosTestUtils;
+import org.apache.zookeeper.server.quorum.auth.MiniKdc;
+import org.apache.zookeeper.test.ClientBase;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This test class is mainly testing the TGT renewal logic implemented
+ * in the org.apache.zookeeper.Login class.
+ */
+public class KerberosTicketRenewalTest {
+
+
+  private static final Logger LOG = LoggerFactory.getLogger(KerberosTicketRenewalTest.class);
+  private static final String JAAS_CONFIG_SECTION = "ClientUsingKerberos";
+  private static final String TICKET_LIFETIME = "5000";
+  private static File testTempDir;
+  private static MiniKdc kdc;
+  private static File kdcWorkDir;
+  private static String PRINCIPAL = KerberosTestUtils.getClientPrincipal();
+
+  TestableKerberosLogin login;
+
+  @BeforeAll
+  public static void setupClass() throws Exception {
+    // by default, we should wait at least 1 minute between subsequent TGT renewals.
+    // changing it to 500ms.
+    System.setProperty(Login.MIN_TIME_BEFORE_RELOGIN_CONFIG_KEY, "500");
+
+    testTempDir = ClientBase.createTmpDir();
+    startMiniKdcAndAddPrincipal();
+
+    String keytabFilePath = FilenameUtils.normalize(KerberosTestUtils.getKeytabFile(), true);
+
+    // note: we use "refreshKrb5Config=true" to refresh the kerberos config in the JVM,
+    // making sure that we use the latest config even if other tests already have been executed
+    // and initialized the kerberos client configs before)
+    String jaasEntries = ""
+      + "ClientUsingKerberos {\n"
+      + "  com.sun.security.auth.module.Krb5LoginModule required\n"
+      + "  storeKey=\"false\"\n"
+      + "  useTicketCache=\"false\"\n"
+      + "  useKeyTab=\"true\"\n"
+      + "  doNotPrompt=\"true\"\n"
+      + "  debug=\"true\"\n"
+      + "  refreshKrb5Config=\"true\"\n"
+      + "  keyTab=\"" + keytabFilePath + "\"\n"
+      + "  principal=\"" + PRINCIPAL + "\";\n"
+      + "};\n";
+    setupJaasConfig(jaasEntries);
+  }
+
+  @AfterAll
+  public static void tearDownClass() {
+    System.clearProperty(Login.MIN_TIME_BEFORE_RELOGIN_CONFIG_KEY);
+    System.clearProperty("java.security.auth.login.config");
+    stopMiniKdc();
+    if (testTempDir != null) {
+      // the testTempDir contains the jaas config file and also the
+      // working folder of the currently running KDC server
+      FileUtils.deleteQuietly(testTempDir);
+    }
+  }
+
+  @AfterEach
+  public void tearDownTest() throws Exception {
+    if (login != null) {
+      login.shutdown();
+      login.logout();
+    }
+  }
+
+
+  /**
+   * We extend the regular Login class to be able to properly control the
+   * "sleeping" between the retry attempts of ticket refresh actions.
+   */
+  private static class TestableKerberosLogin extends Login {
+
+    private AtomicBoolean refreshFailed = new AtomicBoolean(false);
+    private CountDownLatch continueRefreshThread = new CountDownLatch(1);
+
+    public TestableKerberosLogin() throws LoginException {
+      super(JAAS_CONFIG_SECTION, (callbacks) -> {}, new ZKConfig());
+    }
+
+    @Override
+    protected void sleepBeforeRetryFailedRefresh() throws InterruptedException {
+      LOG.info("sleep started due to failed refresh");
+      refreshFailed.set(true);
+      continueRefreshThread.await(20, TimeUnit.SECONDS);
+      LOG.info("sleep due to failed refresh finished");
+    }
+
+    public void assertRefreshFailsEventually(Duration timeout) {
+      assertEventually(timeout, () -> refreshFailed.get());
+    }
+
+    public void continueWithRetryAfterFailedRefresh() {
+      LOG.info("continue refresh thread");
+      continueRefreshThread.countDown();
+    }
+  }
+
+
+  @Test
+  public void shouldLoginUsingKerberos() throws Exception {
+    login = new TestableKerberosLogin();
+    login.startThreadIfNeeded();
+
+    assertPrincipalLoggedIn();
+  }
+
+
+  @Test
+  public void shouldRenewTicketUsingKerberos() throws Exception {
+    login = new TestableKerberosLogin();
+    login.startThreadIfNeeded();
+
+    long initialLoginTime = login.getLastLogin();
+
+    // ticket lifetime is 5sec, so we will trigger ticket renewal in each ~2-3 sec
+    assertTicketRefreshHappenedUntil(Duration.ofSeconds(15));
+
+    assertPrincipalLoggedIn();
+    assertTrue(initialLoginTime < login.getLastLogin());
+  }
+
+
+  @Test
+  public void shouldRecoverIfKerberosNotAvailableForSomeTime() throws Exception {
+    login = new TestableKerberosLogin();
+    login.startThreadIfNeeded();
+
+    assertTicketRefreshHappenedUntil(Duration.ofSeconds(15));
+
+    stopMiniKdc();
+
+    // ticket lifetime is 5sec, so we will trigger ticket renewal in each ~2-3 sec
+    // the very next ticket renewal should fail (as KDC is offline)
+    login.assertRefreshFailsEventually(Duration.ofSeconds(15));
+
+    // now the ticket thread is "sleeping", it will retry the refresh later
+
+    // we restart KDC, then terminate the "sleeping" and expecting
+    // that the next retry should succeed
+    startMiniKdcAndAddPrincipal();
+    login.continueWithRetryAfterFailedRefresh();
+    assertTicketRefreshHappenedUntil(Duration.ofSeconds(15));
+
+    assertPrincipalLoggedIn();
+  }
+
+
+  private void assertPrincipalLoggedIn() {
+    assertEquals(PRINCIPAL, login.getUserName());
+    assertNotNull(login.getSubject());
+    assertEquals(1, login.getSubject().getPrincipals().size());
+    Principal actualPrincipal = login.getSubject().getPrincipals().iterator().next();
+    assertEquals(PRINCIPAL, actualPrincipal.getName());
+  }
+
+  private void assertTicketRefreshHappenedUntil(Duration timeout) {
+    long lastLoginTime = login.getLastLogin();
+    assertEventually(timeout, () -> login.getLastLogin() != lastLoginTime
+      && login.getSubject() != null && !login.getSubject().getPrincipals().isEmpty());
+  }
+
+  private static void assertEventually(Duration timeout, Supplier<Boolean> test) {
+    assertTimeout(timeout, () -> {
+      while (true) {
+        if (test.get()) {
+          return;
+        }
+        Thread.sleep(100);
+      }
+    });
+  }
+
+  public static void startMiniKdcAndAddPrincipal() throws Exception {
+    kdcWorkDir = createTmpDirInside(testTempDir);
+
+    Properties conf = MiniKdc.createConf();
+    conf.setProperty(MAX_TICKET_LIFETIME, TICKET_LIFETIME);
+    conf.setProperty(MIN_TICKET_LIFETIME, TICKET_LIFETIME);
+
+    kdc = new MiniKdc(conf, kdcWorkDir);
+    kdc.start();
+
+    String principalName = PRINCIPAL.substring(0, PRINCIPAL.lastIndexOf("@"));
+    kdc.createPrincipal(new File(KerberosTestUtils.getKeytabFile()), principalName);
+  }
+
+  private static void stopMiniKdc() {
+    if (kdc != null) {
+      kdc.stop();
+      kdc = null;
+    }
+    if (kdcWorkDir != null) {
+      FileUtils.deleteQuietly(kdcWorkDir);
+      kdcWorkDir = null;
+    }
+  }
+
+  private static File createTmpDirInside(File parentDir) throws IOException {
+    File tmpFile = File.createTempFile("test", ".junit", parentDir);
+    // don't delete tmpFile - this ensures we don't attempt to create
+    // a tmpDir with a duplicate name
+    File tmpDir = new File(tmpFile + ".dir");
+    // never true if tmpfile does it's job
+    assertFalse(tmpDir.exists());
+    assertTrue(tmpDir.mkdirs());
+    return tmpDir;
+  }
+
+  private static void setupJaasConfig(String jaasEntries) {
+    try {
+      File saslConfFile = new File(testTempDir, "jaas.conf");
+      FileWriter fwriter = new FileWriter(saslConfFile);
+      fwriter.write(jaasEntries);
+      fwriter.close();
+      System.setProperty("java.security.auth.login.config", saslConfFile.getAbsolutePath());
+    } catch (IOException ioe) {
+      LOG.error("Failed to initialize JAAS conf file", ioe);
+    }
+
+    // refresh the SASL configuration in this JVM (making sure that we use the latest config
+    // even if other tests already have been executed and initialized the SASL configs before)
+    Configuration.getConfiguration().refresh();
+  }
+
+}
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/KerberosTestUtils.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/KerberosTestUtils.java
index 755712b..db95d8d 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/KerberosTestUtils.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/KerberosTestUtils.java
@@ -19,11 +19,17 @@
 package org.apache.zookeeper.server.quorum.auth;
 
 import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.UUID;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
 import org.apache.zookeeper.util.SecurityUtils;
 
 public class KerberosTestUtils {
 
+    private static final boolean IBM_JAVA = System.getProperty("java.vendor").contains("IBM");
+
     private static String keytabFile = new File(System.getProperty("build.test.dir", "build"), UUID.randomUUID().toString()).getAbsolutePath();
 
     public static String getRealm() {
@@ -81,4 +87,59 @@ public class KerberosTestUtils {
         }
     }
 
+    public static class KerberosConfiguration extends Configuration {
+
+        private String principal;
+        private String keytab;
+        private boolean isInitiator;
+
+        private KerberosConfiguration(String principal, File keytab, boolean client) {
+            this.principal = principal;
+            this.keytab = keytab.getAbsolutePath();
+            this.isInitiator = client;
+        }
+
+        public static Configuration createClientConfig(String principal, File keytab) {
+            return new KerberosConfiguration(principal, keytab, true);
+        }
+
+        public static Configuration createServerConfig(String principal, File keytab) {
+            return new KerberosConfiguration(principal, keytab, false);
+        }
+
+        private static String getKrb5LoginModuleName() {
+            return System.getProperty("java.vendor").contains("IBM")
+              ? "com.ibm.security.auth.module.Krb5LoginModule"
+              : "com.sun.security.auth.module.Krb5LoginModule";
+        }
+
+        @Override
+        public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+            Map<String, String> options = new HashMap<String, String>();
+            options.put("principal", principal);
+            options.put("refreshKrb5Config", "true");
+            if (IBM_JAVA) {
+                options.put("useKeytab", keytab);
+                options.put("credsType", "both");
+            } else {
+                options.put("keyTab", keytab);
+                options.put("useKeyTab", "true");
+                options.put("storeKey", "true");
+                options.put("doNotPrompt", "true");
+                options.put("useTicketCache", "true");
+                options.put("renewTGT", "true");
+                options.put("isInitiator", Boolean.toString(isInitiator));
+            }
+            String ticketCache = System.getenv("KRB5CCNAME");
+            if (ticketCache != null) {
+                options.put("ticketCache", ticketCache);
+            }
+            options.put("debug", "true");
+
+            return new AppConfigurationEntry[]{new AppConfigurationEntry(getKrb5LoginModuleName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)};
+        }
+
+    }
+
+
 }
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdc.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdc.java
index 4adcc0b..fdd68b5 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdc.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdc.java
@@ -63,6 +63,7 @@ import org.slf4j.LoggerFactory;
  *   <li>kdc.port=0 (ephemeral port)</li>
  *   <li>instance=DefaultKrbServer</li>
  *   <li>max.ticket.lifetime=86400000 (1 day)</li>
+ *   <li>min.ticket.lifetime=3600000 (1 hour)</li>
  *   <li>max.renewable.lifetime=604800000 (7 days)</li>
  *   <li>transport=TCP</li>
  *   <li>debug=false</li>
@@ -148,6 +149,7 @@ public class MiniKdc {
     public static final String KDC_PORT = "kdc.port";
     public static final String INSTANCE = "instance";
     public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime";
+    public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime";
     public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime";
     public static final String TRANSPORT = "transport";
     public static final String DEBUG = "debug";
@@ -159,11 +161,11 @@ public class MiniKdc {
         PROPERTIES.add(ORG_NAME);
         PROPERTIES.add(ORG_DOMAIN);
         PROPERTIES.add(KDC_BIND_ADDRESS);
-        PROPERTIES.add(KDC_BIND_ADDRESS);
         PROPERTIES.add(KDC_PORT);
         PROPERTIES.add(INSTANCE);
         PROPERTIES.add(TRANSPORT);
         PROPERTIES.add(MAX_TICKET_LIFETIME);
+        PROPERTIES.add(MIN_TICKET_LIFETIME);
         PROPERTIES.add(MAX_RENEWABLE_LIFETIME);
 
         DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost");
@@ -173,6 +175,7 @@ public class MiniKdc {
         DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM");
         DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP");
         DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000");
+        DEFAULT_CONFIG.setProperty(MIN_TICKET_LIFETIME, "3600000");
         DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000");
         DEFAULT_CONFIG.setProperty(DEBUG, "false");
     }
@@ -313,6 +316,10 @@ public class MiniKdc {
             throw new IllegalArgumentException("Need to set transport!");
         }
         simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, conf.getProperty(INSTANCE));
+        long minTicketLifetimeConf = Long.parseLong(conf.getProperty(MIN_TICKET_LIFETIME)) / 1000;
+        simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, minTicketLifetimeConf);
+        long maxTicketLifetimeConf = Long.parseLong(conf.getProperty(MAX_TICKET_LIFETIME)) / 1000;
+        simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, maxTicketLifetimeConf);
         if (conf.getProperty(DEBUG) != null) {
             krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG));
         }
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java
index 4ba6edc..3e57f9a 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java
@@ -23,18 +23,15 @@ import static org.junit.jupiter.api.Assertions.assertNotSame;
 import java.io.File;
 import java.security.Principal;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import javax.security.auth.Subject;
 import javax.security.auth.kerberos.KerberosPrincipal;
-import javax.security.auth.login.AppConfigurationEntry;
-import javax.security.auth.login.Configuration;
 import javax.security.auth.login.LoginContext;
 import org.apache.kerby.kerberos.kerb.keytab.Keytab;
 import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
+import org.apache.zookeeper.server.quorum.auth.KerberosTestUtils.KerberosConfiguration;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 
@@ -47,7 +44,6 @@ import org.junit.jupiter.api.Timeout;
  */
 public class MiniKdcTest extends KerberosSecurityTestcase {
 
-    private static final boolean IBM_JAVA = System.getProperty("java.vendor").contains("IBM");
 
     @Test
     @Timeout(value = 60)
@@ -74,59 +70,6 @@ public class MiniKdcTest extends KerberosSecurityTestcase {
             principals);
     }
 
-    private static class KerberosConfiguration extends Configuration {
-
-        private String principal;
-        private String keytab;
-        private boolean isInitiator;
-
-        private KerberosConfiguration(String principal, File keytab, boolean client) {
-            this.principal = principal;
-            this.keytab = keytab.getAbsolutePath();
-            this.isInitiator = client;
-        }
-
-        public static Configuration createClientConfig(String principal, File keytab) {
-            return new KerberosConfiguration(principal, keytab, true);
-        }
-
-        public static Configuration createServerConfig(String principal, File keytab) {
-            return new KerberosConfiguration(principal, keytab, false);
-        }
-
-        private static String getKrb5LoginModuleName() {
-            return System.getProperty("java.vendor").contains("IBM")
-                ? "com.ibm.security.auth.module.Krb5LoginModule"
-                : "com.sun.security.auth.module.Krb5LoginModule";
-        }
-
-        @Override
-        public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
-            Map<String, String> options = new HashMap<String, String>();
-            options.put("principal", principal);
-            options.put("refreshKrb5Config", "true");
-            if (IBM_JAVA) {
-                options.put("useKeytab", keytab);
-                options.put("credsType", "both");
-            } else {
-                options.put("keyTab", keytab);
-                options.put("useKeyTab", "true");
-                options.put("storeKey", "true");
-                options.put("doNotPrompt", "true");
-                options.put("useTicketCache", "true");
-                options.put("renewTGT", "true");
-                options.put("isInitiator", Boolean.toString(isInitiator));
-            }
-            String ticketCache = System.getenv("KRB5CCNAME");
-            if (ticketCache != null) {
-                options.put("ticketCache", ticketCache);
-            }
-            options.put("debug", "true");
-
-            return new AppConfigurationEntry[]{new AppConfigurationEntry(getKrb5LoginModuleName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)};
-        }
-
-    }
 
     @Test
     @Timeout(value = 60)