DIRKRB-745 Add ktadd cmd for remote admin tool. Contributed by Jiayi Liu.
diff --git a/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrBytes.java b/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrBytes.java
index 105ff74..e987adf 100644
--- a/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrBytes.java
+++ b/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrBytes.java
@@ -6,24 +6,38 @@
  *  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.kerby.xdr.type;
 
 import org.apache.kerby.xdr.XdrDataType;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/*
+ *  From RFC 4506 :
+ *
+ *           0     1     2     3     4     5   ...
+ *        +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
+ *        |        length n       |byte0|byte1|...| n-1 |  0  |...|  0  |
+ *        +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
+ *        |<-------4 bytes------->|<------n bytes------>|<---r bytes--->|
+ *                                |<----n+r (where (n+r) mod 4 = 0)---->|
+ *                                                 VARIABLE-LENGTH OPAQUE
+ */
 
 public class XdrBytes extends XdrSimple<byte[]> {
+    private int padding;
 
     public XdrBytes() {
         this(null);
@@ -35,11 +49,63 @@
 
     @Override
     protected void toValue() throws IOException {
-
+        byte[] bytes = getBytes();
+        byte[] header = new byte[4];
+        System.arraycopy(bytes, 0, header, 0, 4);
+        int byteArrayLen = ByteBuffer.wrap(header).getInt();
+        int paddingBytes = (4 - (byteArrayLen % 4)) % 4;
+        validatePaddingBytes(paddingBytes);
+        setPadding(paddingBytes);
+        
+        if (bytes.length != byteArrayLen + 4 + paddingBytes) {
+            int totalLength = byteArrayLen + 4 + paddingBytes;
+            byte[] resetBytes = ByteBuffer.allocate(totalLength)
+                    .put(getBytes(), 0, totalLength).array();
+            /**reset bytes in case the enum type is in a struct or union*/
+            setBytes(resetBytes);
+        }
+        
+        byte[] content = new byte[byteArrayLen];
+        if (bytes.length > 1) {
+            System.arraycopy(bytes, 4, content, 0, byteArrayLen);
+        }
+        setValue(content);
     }
 
     @Override
-    protected void toBytes() {
+    protected int encodingBodyLength() throws IOException {
+        if (getValue() != null) {
+            padding = (4 - getValue().length % 4) % 4;
+            return getValue().length + padding + 4;
+        }
+        return 0;
+    }
 
+    @Override
+    protected void toBytes() throws IOException {
+        if (getValue() != null) {
+            byte[] bytes = new byte[encodingBodyLength()];
+            int length = getValue().length;
+            bytes[0] = (byte) (length >> 24);
+            bytes[1] = (byte) (length >> 16);
+            bytes[2] = (byte) (length >> 8);
+            bytes[3] = (byte) (length);
+            System.arraycopy(getValue(), 0, bytes, 4, length);
+            setBytes(bytes);
+        }
+    }
+
+    public void setPadding(int padding) {
+        this.padding = padding;
+    }
+
+    public int getPadding() {
+        return padding;
+    }
+
+    private void validatePaddingBytes(int paddingBytes) throws IOException {
+        if (paddingBytes < 0 || paddingBytes > 3) {
+            throw new IOException("Bad padding number: " + paddingBytes + ", should be in [0, 3]");
+        }
     }
 }
diff --git a/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrString.java b/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrString.java
index 539f17c..1da6d3a 100644
--- a/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrString.java
+++ b/kerby-common/kerby-xdr/src/main/java/org/apache/kerby/xdr/type/XdrString.java
@@ -32,7 +32,7 @@
 /*
  *  From RFC 4506 :
  *
- *  0     1     2     3     4     5   ...
+ *           0     1     2     3     4     5   ...
  *        +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
  *        |        length n       |byte0|byte1|...| n-1 |  0  |...|  0  |
  *        +-----+-----+-----+-----+-----+-----+...+-----+-----+...+-----+
diff --git a/kerby-kerb/kerb-admin-server/pom.xml b/kerby-kerb/kerb-admin-server/pom.xml
index 0a3ac9c..183afcc 100644
--- a/kerby-kerb/kerb-admin-server/pom.xml
+++ b/kerby-kerb/kerb-admin-server/pom.xml
@@ -52,5 +52,22 @@
       <artifactId>kerb-common</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.kerby</groupId>
+      <artifactId>kerb-simplekdc</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.kerby</groupId>
+      <artifactId>json-backend</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/kerby-kerb/kerb-admin-server/src/main/java/org/apache/kerby/kerberos/kerb/admin/server/kadmin/AdminServerHandler.java b/kerby-kerb/kerb-admin-server/src/main/java/org/apache/kerby/kerberos/kerb/admin/server/kadmin/AdminServerHandler.java
index 7875fa6..9db44e4 100644
--- a/kerby-kerb/kerb-admin-server/src/main/java/org/apache/kerby/kerberos/kerb/admin/server/kadmin/AdminServerHandler.java
+++ b/kerby-kerb/kerb-admin-server/src/main/java/org/apache/kerby/kerberos/kerb/admin/server/kadmin/AdminServerHandler.java
@@ -30,15 +30,20 @@
 import org.apache.kerby.kerberos.kerb.admin.message.GetprincsRep;
 import org.apache.kerby.kerberos.kerb.admin.message.KadminCode;
 import org.apache.kerby.kerberos.kerb.admin.message.RenamePrincipalRep;
+import org.apache.kerby.kerberos.kerb.admin.message.KeytabMessageCode;
+import org.apache.kerby.kerberos.kerb.admin.message.ExportKeytabRep;
 import org.apache.kerby.xdr.XdrDataType;
 import org.apache.kerby.xdr.XdrFieldInfo;
 import org.apache.kerby.xdr.type.XdrStructType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -91,9 +96,13 @@
                 responseMessage = handleRenamePrincipalReq(localKadmin, fieldInfos);
                 break;
             case GET_PRINCS_REQ:
-                System.out.println("message type getPrincs req");
+                System.out.println("message type: get principals req");
                 responseMessage = handleGetprincsReq(localKadmin, fieldInfos);
                 break;
+            case EXPORT_KEYTAB_REQ:
+                System.out.println("message type: export keytab req");
+                responseMessage = handleExportKeytabReq(localKadmin, fieldInfos);
+                break;
             default:
                 throw new KrbException("AdminMessageType error, can not handle the type: " + type);
         }
@@ -199,6 +208,40 @@
             return responseError;
         }
     }
+    
+    private ByteBuffer handleExportKeytabReq(LocalKadmin localKadmin, XdrFieldInfo[] fieldInfos) throws IOException {
+        String principals = ((String) fieldInfos[2].getValue());
+        
+        if (principals != null) {
+            List<String> princList = stringToList(principals);
+            if (princList.size() != 0) {
+                LOG.info("Exporting keytab file for " + principals + "...");
+                File tempDir = Files.createTempDirectory("ktadd").toFile();
+                File keytabFile = new File(tempDir, princList.get(0)
+                        .replace('/', '-')
+                        .replace('*', '-')
+                        .replace('?', '-')
+                        + ".keytab");
+                try {
+                    localKadmin.exportKeytab(keytabFile, princList);
+                    LOG.info("Create keytab file for principals successfully.");
+                    ByteBuffer responseMessage = infoPackageTool(keytabFile, "exportKeytab");
+                    return responseMessage;
+                } catch (KrbException e) {
+                    String error = "Failed to export keytab. " + e.toString();
+                    ByteBuffer responseError = infoPackageTool(error, "exportKeytab");
+                    return responseError;
+                }
+            } else {
+                String error = "No matched principals.";
+                ByteBuffer responseError = infoPackageTool(error, "exportKeytab");
+                return responseError;
+            }
+        }
+        String error = "Failed to export keytab.";
+        ByteBuffer responseError = infoPackageTool(error, "exportKeytab");
+        return responseError;
+    }
 
     private ByteBuffer infoPackageTool(String message, String dealType) throws IOException {
         AdminMessage adminMessage = null;
@@ -226,6 +269,23 @@
 
         return KadminCode.encodeMessage(adminMessage);
     }
+    
+    private ByteBuffer infoPackageTool(File keytabFile, String dealType) throws IOException {
+        AdminMessage adminMessage = null;
+        XdrFieldInfo[] xdrFieldInfos = new XdrFieldInfo[3];
+        if ("exportKeytab".equals(dealType)) {
+            adminMessage = new ExportKeytabRep();
+            xdrFieldInfos[0] = new XdrFieldInfo(0, XdrDataType.ENUM, AdminMessageType.EXPORT_KEYTAB_REP);
+        }
+        
+        xdrFieldInfos[1] = new XdrFieldInfo(1, XdrDataType.INTEGER, 1);
+        xdrFieldInfos[2] = new XdrFieldInfo(2, XdrDataType.BYTES, Files.readAllBytes(keytabFile.toPath()));
+
+        KeytabMessageCode value = new KeytabMessageCode(xdrFieldInfos);
+        adminMessage.setMessageBuffer(ByteBuffer.wrap(value.encode()));
+
+        return KadminCode.encodeMessage(adminMessage);
+    }
 
     private String listToString(List<String> list) {
         if (list.isEmpty()) {
@@ -238,4 +298,22 @@
         }
         return result.toString();
     }
+    
+    private List<String> stringToList(String str) {
+        if (str == null || str.isEmpty()) {
+            return null;
+        }
+        String[] principals = str.split(" ");
+        for (int i = 0; i < principals.length; i++) {
+            principals[i] = fixPrincipal(principals[i]);
+        }
+        return Arrays.asList(principals);
+    }
+
+    private String fixPrincipal(String principal) {
+        if (!principal.contains("@")) {
+            principal += "@" + adminServerContext.getAdminRealm();
+        }
+        return principal;
+    }
 }
diff --git a/kerby-kerb/kerb-admin-server/src/test/java/org/apache/kerby/kerberos/kerb/admin/server/RemoteKadminTest.java b/kerby-kerb/kerb-admin-server/src/test/java/org/apache/kerby/kerberos/kerb/admin/server/RemoteKadminTest.java
new file mode 100644
index 0000000..a539e1e
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/java/org/apache/kerby/kerberos/kerb/admin/server/RemoteKadminTest.java
@@ -0,0 +1,255 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.server;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.kerby.kerberos.kerb.admin.AuthUtil;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.local.LocalKadminImpl;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminClient;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminConfig;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminUtil;
+import org.apache.kerby.kerberos.kerb.admin.server.kadmin.AdminServer;
+import org.apache.kerby.kerberos.kerb.admin.server.kadmin.AdminServerConfig;
+import org.apache.kerby.kerberos.kerb.identity.backend.BackendConfig;
+import org.apache.kerby.kerberos.kerb.keytab.Keytab;
+import org.apache.kerby.kerberos.kerb.server.KdcConfig;
+import org.apache.kerby.kerberos.kerb.server.KdcServer;
+import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
+import org.apache.kerby.kerberos.kerb.transport.KrbNetwork;
+import org.apache.kerby.kerberos.kerb.transport.KrbTransport;
+import org.apache.kerby.kerberos.kerb.transport.TransportPair;
+import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
+import org.apache.kerby.util.NetworkUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+public class RemoteKadminTest {
+    private static final Logger LOG = LoggerFactory.getLogger(RemoteKadminTest.class);
+    
+    protected static File testDir = new File(System.getProperty("test.dir", "target"));
+    private static File testClassDir = new File(testDir, "test-classes");
+    private static File confDir = new File(testClassDir, "conf");
+    private static File workDir = new File(testDir, "work-dir");
+    
+    private static final String SERVER_HOST = "localhost";
+
+    private static final String ADMIN_PRINCIPAL = "kadmin/EXAMPLE.COM@EXAMPLE.COM";
+    private static final String PROTOCOL_PRINCIPAL = "adminprotocol/localhost@EXAMPLE.COM";
+
+    private static KdcServer kdcServer;
+    private static LocalKadminImpl localKadmin;
+    private static AdminServer adminServer;
+
+    private static SimpleKdcServer buildMiniKdc() throws Exception {
+        KdcConfig config = new KdcConfig();
+        BackendConfig backendConfig = new BackendConfig();
+        backendConfig.addIniConfig(new File(confDir, "backend.conf"));
+        SimpleKdcServer kdc = new SimpleKdcServer(config, backendConfig);
+        kdc.setWorkDir(workDir);
+        
+        kdc.setKdcHost(SERVER_HOST);
+        int kdcPort = NetworkUtil.getServerPort();
+        kdc.setAllowTcp(true);
+        kdc.setAllowUdp(false);
+        kdc.setKdcTcpPort(kdcPort);
+        LOG.info("Starting KDC server at " + SERVER_HOST + ":" + kdcPort);
+        kdc.init();
+        return kdc;
+    }
+    
+    private static void buildLocalKadmin() throws Exception {
+        localKadmin = new LocalKadminImpl(kdcServer.getKdcSetting(), kdcServer.getIdentityService());
+        localKadmin.addPrincipal(PROTOCOL_PRINCIPAL);
+        localKadmin.exportKeytab(new File(workDir, "protocol.keytab"), PROTOCOL_PRINCIPAL);
+        localKadmin.exportKeytab(new File(workDir, "admin.keytab"), ADMIN_PRINCIPAL);
+    }
+    
+    private static AdminServer buildAdminServer() throws Exception {
+        AdminServer server = new AdminServer(confDir);
+        AdminServerConfig config = server.getAdminServerConfig();
+        server.setAdminHost(config.getAdminHost());
+        server.setAllowTcp(true);
+        server.setAllowUdp(false);
+        server.setAdminServerPort(config.getAdminPort());
+
+        server.init();
+        return server;
+    }
+
+    private AdminClient buildKadminRemoteClient() throws Exception {
+        final AdminConfig config = new AdminConfig();
+        config.addKrb5Config(new File(confDir, "adminClient.conf"));
+        AdminClient adminClient = new AdminClient(config);
+        adminClient.setAdminRealm(config.getAdminRealm());
+        adminClient.setAllowTcp(true);
+        adminClient.setAllowUdp(false);
+        adminClient.setAdminTcpPort(config.getAdminPort());
+
+        adminClient.init();
+        doSaslHandShake(adminClient, config);
+        return adminClient;
+    }
+    
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        workDir.mkdirs();
+        
+        kdcServer = buildMiniKdc();
+        kdcServer.start();
+
+        buildLocalKadmin();
+        
+        adminServer = buildAdminServer();
+        adminServer.start();
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() throws Exception {
+        adminServer.stop();
+        kdcServer.stop();
+        FileUtils.deleteDirectory(workDir);
+    }
+
+    @Test
+    public void remoteListPrincipalTest() throws Exception {
+        AdminClient adminClient = buildKadminRemoteClient();
+        List<String> principals = adminClient.requestGetprincs();
+        assertTrue(principals.contains("kadmin/EXAMPLE.COM@EXAMPLE.COM"));
+        assertTrue(principals.contains("krbtgt/EXAMPLE.COM@EXAMPLE.COM"));
+        assertTrue(principals.contains("adminprotocol/localhost@EXAMPLE.COM"));
+
+        principals = adminClient.requestGetprincsWithExp("k*");
+        assertTrue(principals.contains("kadmin/EXAMPLE.COM@EXAMPLE.COM"));
+        assertTrue(principals.contains("krbtgt/EXAMPLE.COM@EXAMPLE.COM"));
+        assertFalse(principals.contains("adminprotocol/localhost@EXAMPLE.COM"));
+    }
+
+    @Test
+    public void remoteModifyPrincipalTest() throws Exception {
+        AdminClient adminClient = buildKadminRemoteClient();
+        String testPrincipal = "test/EXAMPLE.COM";
+        String renamePrincipal = "test_rename/EXAMPLE.COM";
+        adminClient.requestAddPrincipal(testPrincipal);
+        assertTrue("Remote kadmin add principal test failed.",
+                localKadmin.getPrincipals().contains(testPrincipal + "@EXAMPLE.COM"));
+
+        adminClient.requestRenamePrincipal(testPrincipal, renamePrincipal);
+        assertTrue("Remote kadmin rename principal test failed, the renamed principal does not exist.",
+                localKadmin.getPrincipals().contains(renamePrincipal + "@EXAMPLE.COM"));
+        assertFalse("Remote kadmin rename principal test failed, the old principal still exists.",
+                localKadmin.getPrincipals().contains(testPrincipal + "@EXAMPLE.COM"));
+
+        adminClient.requestDeletePrincipal(renamePrincipal);
+        assertFalse("Remote kadmin delete principal test failed.",
+                localKadmin.getPrincipals().contains(renamePrincipal + "@EXAMPLE.COM"));
+    }
+
+    @Test
+    public void remoteKtaddTest() throws Exception {
+        AdminClient adminClient = buildKadminRemoteClient();
+        String testPrincipal = "test/EXAMPLE.COM";
+        File keytabOutput = new File(testDir, "test.keytab");
+        localKadmin.addPrincipal(testPrincipal);
+        adminClient.requestExportKeytab(keytabOutput, testPrincipal);
+        Keytab localKeytab = Keytab.loadKeytab(keytabOutput);
+        List<String> principalNames = localKeytab.getPrincipals()
+                .stream().map(PrincipalName::getName).collect(Collectors.toList());
+        assertTrue(principalNames.contains(testPrincipal + "@EXAMPLE.COM"));
+    }
+
+    private void doSaslHandShake(AdminClient adminClient, AdminConfig config) throws Exception {
+        TransportPair tpair = AdminUtil.getTransportPair(adminClient.getSetting());
+        KrbNetwork network = new KrbNetwork();
+        network.setSocketTimeout(adminClient.getSetting().getTimeout());
+        KrbTransport transport = network.connect(tpair);
+        Subject subject = AuthUtil.loginUsingKeytab(ADMIN_PRINCIPAL, new File(config.getKeyTabFile()));
+        Subject.doAs(subject, (PrivilegedAction<Object>) () -> {
+            try {
+                Map<String, String> props = new HashMap<>();
+                props.put(Sasl.QOP, "auth-conf");
+                props.put(Sasl.SERVER_AUTH, "true");
+                SaslClient saslClient = null;
+
+                String protocol = config.getProtocol();
+                String serverName = config.getServerName();
+                saslClient = Sasl.createSaslClient(new String[]{"GSSAPI"}, null,
+                        protocol, serverName, props, null);
+                if (saslClient == null) {
+                    System.out.println("Unable to find client implementation for: GSSAPI");
+                    return null;
+                }
+                byte[] response;
+                response = saslClient.hasInitialResponse()
+                        ? saslClient.evaluateChallenge(new byte[0]) : new byte[0];
+
+                sendMessage(response, saslClient, transport);
+
+                ByteBuffer message = transport.receiveMessage();
+
+                while (!saslClient.isComplete()) {
+                    int ssComplete = message.getInt();
+                    if (ssComplete == 0) {
+                        System.out.println("Sasl Server completed");
+                    }
+                    byte[] arr = new byte[message.remaining()];
+                    message.get(arr);
+                    byte[] challenge = saslClient.evaluateChallenge(arr);
+
+                    sendMessage(challenge, saslClient, transport);
+
+                    if (!saslClient.isComplete()) {
+                        message = transport.receiveMessage();
+                    }
+                }
+            } catch (Exception e) {
+                LOG.warn(e.getMessage());
+            }
+            return null;
+        });
+    }
+
+    private void sendMessage(byte[] challenge, SaslClient saslClient, KrbTransport transport) throws Exception {
+        ByteBuffer buffer = ByteBuffer.allocate(challenge.length + 8);
+        buffer.putInt(challenge.length + 4);
+        int scComplete = saslClient.isComplete() ? 0 : 1;
+
+        buffer.putInt(scComplete);
+        buffer.put(challenge);
+        buffer.flip();
+        transport.sendMessage(buffer);
+    }
+}
diff --git a/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminClient.conf b/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminClient.conf
new file mode 100644
index 0000000..e970ae6
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminClient.conf
@@ -0,0 +1,7 @@
+[libdefaults]
+    default_realm = EXAMPLE.COM
+    admin_host = localhost
+    admin_port = 65417
+    keytab_file = target/work-dir/admin.keytab
+    protocol = adminprotocol
+    server_name = localhost
\ No newline at end of file
diff --git a/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminServer.conf b/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminServer.conf
new file mode 100644
index 0000000..06a96c7
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/resources/conf/adminServer.conf
@@ -0,0 +1,8 @@
+[libdefaults]
+    default_realm = EXAMPLE.COM
+    admin_realm = EXAMPLE.COM
+    admin_host = localhost
+    admin_port = 65417
+    keytab_file = target/work-dir/protocol.keytab
+    protocol = adminprotocol
+    server_name = localhost
\ No newline at end of file
diff --git a/kerby-kerb/kerb-admin-server/src/test/resources/conf/backend.conf b/kerby-kerb/kerb-admin-server/src/test/resources/conf/backend.conf
new file mode 100644
index 0000000..797bf0c
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/resources/conf/backend.conf
@@ -0,0 +1,2 @@
+kdc_identity_backend = org.apache.kerby.kerberos.kdc.identitybackend.JsonIdentityBackend
+backend.json.dir = target/work-dir/jsonbackend
\ No newline at end of file
diff --git a/kerby-kerb/kerb-admin-server/src/test/resources/conf/kdc.conf b/kerby-kerb/kerb-admin-server/src/test/resources/conf/kdc.conf
new file mode 100644
index 0000000..a3948fa
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/resources/conf/kdc.conf
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+
+[kdcdefaults]
+  kdc_host = localhost
+  kdc_udp_port = 8866
+  kdc_tcp_port = 8866
+  kdc_realm = EXAMPLE.COM
diff --git a/kerby-kerb/kerb-admin-server/src/test/resources/conf/krb5.conf b/kerby-kerb/kerb-admin-server/src/test/resources/conf/krb5.conf
new file mode 100644
index 0000000..def3c96
--- /dev/null
+++ b/kerby-kerb/kerb-admin-server/src/test/resources/conf/krb5.conf
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+[libdefaults]
+    kdc_realm = EXAMPLE.COM
+    default_realm = EXAMPLE.COM
+    udp_preference_limit = 4096
+    kdc_tcp_port = 8866
+    kdc_udp_port = 8866
+
+[realms]
+    EXAMPLE.COM = {
+        kdc = localhost:8866
+    }
diff --git a/kerby-kerb/kerb-admin/pom.xml b/kerby-kerb/kerb-admin/pom.xml
index 6f2f599..84b110f 100644
--- a/kerby-kerb/kerb-admin/pom.xml
+++ b/kerby-kerb/kerb-admin/pom.xml
@@ -42,5 +42,15 @@
       <artifactId>kerby-xdr</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.jline</groupId>
+      <artifactId>jline</artifactId>
+      <version>${jline.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.jcraft</groupId>
+      <artifactId>jsch</artifactId>
+      <version>${jsch.version}</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/RemoteAdminClientTool.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/RemoteAdminClientTool.java
index 96d68ce..e8d8531 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/RemoteAdminClientTool.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/RemoteAdminClientTool.java
@@ -23,12 +23,12 @@
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminClient;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminConfig;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminUtil;
-import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteAddPrincipalCommand;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteCommand;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteAddPrincipalCommand;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteDeletePrincipalCommand;
-import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteGetprincsCommand;
-import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemotePrintUsageCommand;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteRenamePrincipalCommand;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteGetprincsCommand;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.command.RemoteKeytabAddCommand;
 import org.apache.kerby.kerberos.kerb.common.KrbUtil;
 import org.apache.kerby.kerberos.kerb.server.KdcConfig;
 import org.apache.kerby.kerberos.kerb.server.KdcUtil;
@@ -36,6 +36,14 @@
 import org.apache.kerby.kerberos.kerb.transport.KrbTransport;
 import org.apache.kerby.kerberos.kerb.transport.TransportPair;
 import org.apache.kerby.util.OSUtil;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.Completer;
+import org.jline.reader.UserInterruptException;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.impl.completer.StringsCompleter;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -50,7 +58,6 @@
 import java.security.PrivilegedAction;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Scanner;
 
 /**
  * Command use of remote admin
@@ -59,7 +66,7 @@
     private static final Logger LOG = LoggerFactory.getLogger(RemoteAdminClientTool.class);
     private static final byte[] EMPTY = new byte[0];
     private static KrbTransport transport;
-    private static final String PROMPT = RemoteAdminClientTool.class.getSimpleName() + ".local:";
+    private static final String PROMPT = RemoteAdminClientTool.class.getSimpleName() + ".remote";
     private static final String USAGE = (OSUtil.isWindows()
         ? "Usage: bin\\remote-admin-client.cmd" : "Usage: sh bin/remote-admin-client.sh")
         + " <conf-file>\n"
@@ -77,8 +84,10 @@
         + "                         Delete principal\n"
         + "rename_principal, renprinc\n"
         + "                         Rename principal\n"
-        + "listprincs\n"
-        + "          List principals\n";
+        + "list_principals, listprincs\n"
+        + "                         List principals\n"
+        + "ktadd, xst\n"
+        + "                         Add entry(s) to a keytab\n";
 
     public static void main(String[] args) throws Exception {
         AdminClient adminClient;
@@ -205,13 +214,28 @@
 
         System.out.println("enter \"command\" to see legal commands.");
 
-        try (Scanner scanner = new Scanner(System.in, "UTF-8")) {
-            String input = scanner.nextLine();
+        Completer completer = new StringsCompleter("add_principal", "delete_principal", "rename_principal",
+                "list_principals", "ktadd");
 
-            while (!(input.equals("quit") || input.equals("exit") || input.equals("q"))) {
-                excute(adminClient, input);
-                System.out.print(PROMPT);
-                input = scanner.nextLine();
+        Terminal terminal = null;
+        try {
+            terminal = TerminalBuilder.terminal();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        LineReader lineReader = LineReaderBuilder.builder().completer(completer).terminal(terminal).build();
+
+        while (true) {
+            try {
+                String line = lineReader.readLine(PROMPT + ": ");
+                if ("quit".equals(line) || "exit".equals(line) || "q".equals(line)) {
+                    break;
+                }
+                execute(adminClient, line);
+            } catch (UserInterruptException | EndOfFileException ex) {
+                break;
+            } catch (KrbException e) {
+                System.err.println(e.getMessage());
             }
         }
     }
@@ -235,7 +259,7 @@
         }
     }
 
-    private static void excute(AdminClient adminClient, String input) throws KrbException {
+    private static void execute(AdminClient adminClient, String input) throws KrbException {
         input = input.trim();
         if (input.startsWith("command")) {
             System.out.println(LEGAL_COMMANDS);
@@ -253,10 +277,12 @@
         } else if (input.startsWith("rename_principal")
             || input.startsWith("renprinc")) {
             executor = new RemoteRenamePrincipalCommand(adminClient);
-        } else if (input.startsWith("list_principals")) {
+        } else if (input.startsWith("list_principals")
+            || input.startsWith("listprincs")) {
             executor = new RemoteGetprincsCommand(adminClient);
-        } else if (input.startsWith("listprincs")) {
-            executor = new RemotePrintUsageCommand();
+        } else if (input.startsWith("ktadd")
+            || input.startsWith("xst")) {
+            executor = new RemoteKeytabAddCommand(adminClient);
         } else {
             System.out.println(LEGAL_COMMANDS);
             return;
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/local/AdminHelper.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/local/AdminHelper.java
index c69148d..b3fc01f 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/local/AdminHelper.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/local/AdminHelper.java
@@ -32,6 +32,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Date;
 import java.util.List;
 import java.util.regex.Pattern;
@@ -101,6 +102,24 @@
     }
 
     /**
+     * Load keytab from Input stream.
+     *
+     * @param keytabInputStream The Input stream
+     * @return The keytab load from keytab file
+     * @throws KrbException If there is a problem loading the Input stream
+     */
+    public static Keytab loadKeytab(InputStream keytabInputStream) throws KrbException {
+        Keytab keytab;
+        try {
+            keytab = Keytab.loadKeytab(keytabInputStream);
+        } catch (IOException e) {
+            throw new KrbException("Failed to load keytab", e);
+        }
+
+        return keytab;
+    }
+
+    /**
      * If keytab file does not exist, create a new keytab,
      * otherwise load keytab from keytab file.
      *
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminClient.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminClient.java
index 01c336d..ee15058 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminClient.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminClient.java
@@ -201,4 +201,14 @@
         List<String> principalLists = remote.getPrincipals(exp);
         return principalLists;
     }
+    
+    public void requestExportKeytab(File keytabFile, String principal) throws KrbException {
+        Kadmin remote = new RemoteKadminImpl(innerClient);
+        remote.exportKeytab(keytabFile, principal);
+    }
+
+    public void requestExportKeytab(File keytabFile, List<String> principals) throws KrbException {
+        Kadmin remote = new RemoteKadminImpl(innerClient);
+        remote.exportKeytab(keytabFile, principals);
+    }
 }
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminHandler.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminHandler.java
index 9debfdd..ddcb362 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminHandler.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/AdminHandler.java
@@ -21,10 +21,11 @@
 
 import org.apache.kerby.kerberos.kerb.KrbException;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.AdminRequest;
-import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageCode;
-import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageType;
 import org.apache.kerby.kerberos.kerb.admin.message.AdminReq;
 import org.apache.kerby.kerberos.kerb.admin.message.KadminCode;
+import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageCode;
+import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageType;
+import org.apache.kerby.kerberos.kerb.admin.message.KeytabMessageCode;
 import org.apache.kerby.xdr.XdrFieldInfo;
 import org.apache.kerby.xdr.type.XdrStructType;
 
@@ -147,6 +148,35 @@
 
         return princalsList;
     }
+    
+    public byte[] onResponseMessageForBytesArray(AdminRequest adminRequest,
+                                 ByteBuffer responseMessage) throws KrbException {
+        byte[] keytabFileBytes = null;
+        
+        XdrStructType decoded = new KeytabMessageCode();
+        try {
+            decoded.decode(responseMessage);
+        } catch (IOException e) {
+            throw new KrbException("On response message failed.", e);
+        }
+        XdrFieldInfo[] fieldInfos = decoded.getValue().getXdrFieldInfos();
+        AdminMessageType type = (AdminMessageType) fieldInfos[0].getValue();
+        
+        switch (type) {
+            case EXPORT_KEYTAB_REP:
+                if (adminRequest.getAdminReq().getAdminMessageType() == AdminMessageType.EXPORT_KEYTAB_REQ) {
+                    keytabFileBytes = (byte[]) fieldInfos[2].getValue();
+                } else {
+                    throw new KrbException("Response message type error: need "
+                            + AdminMessageType.EXPORT_KEYTAB_REP);
+                }
+                break;
+            default:
+                throw new KrbException("Response message type error: " + type);
+        }
+        
+        return keytabFileBytes;
+    }
 
     /**
      * Send message to kdc.
@@ -159,4 +189,6 @@
                                         ByteBuffer requestMessage) throws IOException;
 
     protected abstract List<String> handleRequestForList(AdminRequest adminRequest) throws KrbException;
+    
+    protected abstract byte[] handleRequestForBytes(AdminRequest adminRequest) throws KrbException;
 }
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/RemoteKadminImpl.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/RemoteKadminImpl.java
index ccc3419..69846b3 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/RemoteKadminImpl.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/RemoteKadminImpl.java
@@ -22,22 +22,28 @@
 import org.apache.kerby.KOptions;
 import org.apache.kerby.kerberos.kerb.KrbException;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.Kadmin;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.local.AdminHelper;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.impl.DefaultAdminHandler;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.impl.InternalAdminClient;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.AddPrincipalRequest;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.AdminRequest;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.ExportKeytabRequest;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.DeletePrincipalRequest;
-import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.GetprincsRequest;
 import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.RenamePrincipalRequest;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.request.GetprincsRequest;
 import org.apache.kerby.kerberos.kerb.common.KrbUtil;
+import org.apache.kerby.kerberos.kerb.keytab.Keytab;
 import org.apache.kerby.kerberos.kerb.transport.KrbNetwork;
 import org.apache.kerby.kerberos.kerb.transport.KrbTransport;
 import org.apache.kerby.kerberos.kerb.transport.TransportPair;
+import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -120,13 +126,26 @@
     @Override
     public void exportKeytab(File keytabFile,
                              String principal) throws KrbException {
-
+        exportKeytab(keytabFile, Collections.singletonList(principal));
     }
 
     @Override
     public void exportKeytab(File keytabFile,
                              List<String> principals) throws KrbException {
-
+        String principalsStr = listToString(principals);
+        AdminRequest exportKeytabRequest = new ExportKeytabRequest(principalsStr);
+        exportKeytabRequest.setTransport(transport);
+        AdminHandler adminHandler = new DefaultAdminHandler();
+        byte[] keytabFileBytes = adminHandler.handleRequestForBytes(exportKeytabRequest);
+        
+        Keytab keytab = AdminHelper.loadKeytab(new ByteArrayInputStream(keytabFileBytes));
+        Keytab outputKeytab = AdminHelper.createOrLoadKeytab(keytabFile);
+        
+        // If original keytab file exists, we need to merge keytab entries 
+        for (PrincipalName principal: keytab.getPrincipals()) {
+            outputKeytab.addKeytabEntries(keytab.getKeytabEntries(principal));
+        }
+        AdminHelper.storeKeytab(outputKeytab, keytabFile);
     }
 
     @Override
@@ -206,4 +225,16 @@
     public void release() throws KrbException {
 
     }
+
+    private String listToString(List<String> list) {
+        if (list.isEmpty()) {
+            return null;
+        }
+        //Both speed and safety,so use StringBuilder
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < list.size(); i++) {
+            result.append(list.get(i)).append(" ");
+        }
+        return result.toString();
+    }
 }
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/command/RemoteKeytabAddCommand.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/command/RemoteKeytabAddCommand.java
new file mode 100644
index 0000000..0ff489e
--- /dev/null
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/command/RemoteKeytabAddCommand.java
@@ -0,0 +1,94 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.kadmin.remote.command;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.admin.kadmin.remote.AdminClient;
+
+import java.io.File;
+import java.util.List;
+
+public class RemoteKeytabAddCommand extends RemoteCommand {
+    private static final String USAGE = "Usage: ktadd [-k[eytab] keytab] "
+            + "[principal | -glob princ-exp] [...]\n"
+            + "\tExample:\n"
+            + "\t\tktadd hello@TEST.COM -k /keytab/location\n";
+    private static final String DEFAULT_KEYTAB_FILE_LOCATION = "/etc/krb5.keytab";
+    
+    public RemoteKeytabAddCommand(AdminClient adminClient) {
+        super(adminClient);
+    }
+
+    @Override
+    public void execute(String input) throws KrbException {
+        String[] items = input.split("\\s+");
+        
+        if (items.length < 2) {
+            System.err.println(USAGE);
+            return;
+        }
+        
+        String principal = null;
+        String keytabFileLocation = null;
+        boolean glob = false;
+        
+        int index = 1;
+        while (index < items.length) {
+            String command = items[index];
+            if (command.equals("-k")) {
+                index++;
+                if (index >= items.length) {
+                    System.err.println(USAGE);
+                    return;
+                }
+                keytabFileLocation = items[index].trim();
+            } else if (command.equals("-glob")) {
+                glob = true;
+            } else if (!command.startsWith("-")) {
+                principal = command;
+            }
+            index++;
+        }
+        
+        if (keytabFileLocation == null) {
+            keytabFileLocation = DEFAULT_KEYTAB_FILE_LOCATION;
+        }
+        File keytabFile = new File(keytabFileLocation);
+        
+        if (principal == null) {
+            System.out.println((glob ? "princ-exp" : "principal") + " not specified!");
+            System.err.println(USAGE);
+            return;
+        }
+        
+        try {
+            if (glob) {
+                List<String> principals = adminClient.requestGetprincsWithExp(principal);
+                adminClient.requestExportKeytab(keytabFile, principals);
+            } else {
+                adminClient.requestExportKeytab(keytabFile, principal);
+            }
+            System.out.println("Export Keytab to " + keytabFileLocation);
+        } catch (KrbException e) {
+            System.err.println("Principal \"" + principal + "\" fail to add entry to keytab. " 
+                    + e.toString());
+        }
+    }
+}
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/impl/DefaultAdminHandler.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/impl/DefaultAdminHandler.java
index 3d05b50..41615a7 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/impl/DefaultAdminHandler.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/impl/DefaultAdminHandler.java
@@ -76,4 +76,20 @@
 
         return prinicalList;
     }
+
+    @Override
+    protected byte[] handleRequestForBytes(AdminRequest adminRequest) throws KrbException {
+        super.handleRequest(adminRequest);
+        
+        KrbTransport transport = adminRequest.getTransport();
+        ByteBuffer receiveMessage = null;
+        byte[] keytabFileBytes;
+        try {
+            receiveMessage = transport.receiveMessage();
+            keytabFileBytes = super.onResponseMessageForBytesArray(adminRequest, receiveMessage);
+        } catch (IOException e) {
+            throw new KrbException("Admin receives response message failed", e);
+        }
+        return keytabFileBytes;
+    }
 }
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/request/ExportKeytabRequest.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/request/ExportKeytabRequest.java
new file mode 100644
index 0000000..eb75670
--- /dev/null
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/kadmin/remote/request/ExportKeytabRequest.java
@@ -0,0 +1,61 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.kadmin.remote.request;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageCode;
+import org.apache.kerby.kerberos.kerb.admin.message.AdminMessageType;
+import org.apache.kerby.kerberos.kerb.admin.message.ExportKeytabReq;
+import org.apache.kerby.xdr.XdrDataType;
+import org.apache.kerby.xdr.XdrFieldInfo;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class ExportKeytabRequest extends AdminRequest {
+    
+    public ExportKeytabRequest(String principal) {
+        super(principal);
+    }
+
+    @Override
+    public void process() throws KrbException {
+        super.process();
+
+        ExportKeytabReq exportKeytabReq = new ExportKeytabReq();
+
+        XdrFieldInfo[] xdrFieldInfos = new XdrFieldInfo[3];
+        xdrFieldInfos[0] = new XdrFieldInfo(0, XdrDataType.ENUM, AdminMessageType.EXPORT_KEYTAB_REQ);
+        xdrFieldInfos[1] = new XdrFieldInfo(1, XdrDataType.INTEGER, 1);
+        xdrFieldInfos[2] = new XdrFieldInfo(2, XdrDataType.STRING, getPrincipal());
+
+        AdminMessageCode value = new AdminMessageCode(xdrFieldInfos);
+        byte[] encodeBytes;
+        try {
+            encodeBytes = value.encode();
+        } catch (IOException e) {
+            throw new KrbException("Xdr encode error when generate get principals request.", e);
+        }
+        ByteBuffer messageBuffer = ByteBuffer.wrap(encodeBytes);
+        exportKeytabReq.setMessageBuffer(messageBuffer);
+
+        setAdminReq(exportKeytabReq);
+    }
+}
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/AdminMessageType.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/AdminMessageType.java
index f44187e..0688179 100644
--- a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/AdminMessageType.java
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/AdminMessageType.java
@@ -42,7 +42,9 @@
     RENAME_PRINCIPAL_REQ(4),
     RENAME_PRINCIPAL_REP(5),
     GET_PRINCS_REQ(6),
-    GET_PRINCS_REP(7);
+    GET_PRINCS_REP(7),
+    EXPORT_KEYTAB_REQ(8),
+    EXPORT_KEYTAB_REP(9);
 
     private int value;
 
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabRep.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabRep.java
new file mode 100644
index 0000000..f9356b2
--- /dev/null
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabRep.java
@@ -0,0 +1,26 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.message;
+
+public class ExportKeytabRep extends AdminRep {
+    public ExportKeytabRep() {
+        super(AdminMessageType.EXPORT_KEYTAB_REP);
+    }
+}
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabReq.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabReq.java
new file mode 100644
index 0000000..cd687a0
--- /dev/null
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/ExportKeytabReq.java
@@ -0,0 +1,26 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.message;
+
+public class ExportKeytabReq extends AdminReq {
+    public ExportKeytabReq() {
+        super(AdminMessageType.EXPORT_KEYTAB_REQ);
+    }
+}
diff --git a/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/KeytabMessageCode.java b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/KeytabMessageCode.java
new file mode 100644
index 0000000..234e68d
--- /dev/null
+++ b/kerby-kerb/kerb-admin/src/main/java/org/apache/kerby/kerberos/kerb/admin/message/KeytabMessageCode.java
@@ -0,0 +1,82 @@
+/**
+ *  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.kerby.kerberos.kerb.admin.message;
+
+import org.apache.kerby.xdr.XdrDataType;
+import org.apache.kerby.xdr.XdrFieldInfo;
+import org.apache.kerby.xdr.type.XdrStructType;
+import org.apache.kerby.xdr.type.XdrType;
+import org.apache.kerby.xdr.type.XdrInteger;
+import org.apache.kerby.xdr.type.XdrString;
+import org.apache.kerby.xdr.type.XdrBytes;
+import org.apache.kerby.xdr.type.AbstractXdrType;
+
+/**
+ * An extend XdrStructType to encode and decode ExportKeytab message.
+ */
+public class KeytabMessageCode extends XdrStructType {
+    public KeytabMessageCode() {
+        super(XdrDataType.STRUCT);
+    }
+
+    public KeytabMessageCode(XdrFieldInfo[] fieldInfos) {
+        super(XdrDataType.STRUCT, fieldInfos);
+    }
+
+    @Override
+    protected void getStructTypeInstance(XdrType[] fields, XdrFieldInfo[] fieldInfos) {
+        for (int i = 0; i < fieldInfos.length; i++) {
+            switch (fieldInfos[i].getDataType()) {
+                case INTEGER:
+                    fields[i] = new XdrInteger((Integer) fieldInfos[i].getValue());
+                    break;
+                case ENUM:
+                    fields[i] = new AdminMessageEnum((AdminMessageType) fieldInfos[i].getValue());
+                    break;
+                case STRING:
+                    fields[i] = new XdrString((String) fieldInfos[i].getValue());
+                    break;
+                case BYTES:
+                    fields[i] = new XdrBytes((byte[]) fieldInfos[i].getValue());
+                    break;
+                default:
+                    fields[i] = null;
+            }
+        }
+    }
+
+    @Override
+    protected XdrStructType fieldsToValues(AbstractXdrType[] fields) {
+        XdrFieldInfo[] xdrFieldInfos = new XdrFieldInfo[3];
+        xdrFieldInfos[0] = new XdrFieldInfo(0, XdrDataType.ENUM, fields[0].getValue());
+        xdrFieldInfos[1] = new XdrFieldInfo(1, XdrDataType.INTEGER, fields[1].getValue());
+        xdrFieldInfos[2] = new XdrFieldInfo(2, XdrDataType.BYTES, fields[2].getValue());
+        return new KeytabMessageCode(xdrFieldInfos);
+    }
+
+    @Override
+    protected AbstractXdrType[] getAllFields() {
+        AbstractXdrType[] fields = new AbstractXdrType[4];
+        fields[0] = new AdminMessageEnum();
+        fields[1] = new XdrInteger();
+        fields[2] = new XdrBytes();
+        return fields;
+    }
+}