diff --git a/admin/add.c b/admin/add.c
index 13580b9bb..5f1920ff8 100644
--- a/admin/add.c
+++ b/admin/add.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997-2005 Kungliga Tekniska Högskolan
+ * Copyright (c) 1997-2022 Kungliga Tekniska Högskolan
  * (Royal Institute of Technology, Stockholm, Sweden).
  * All rights reserved.
  *
@@ -32,6 +32,8 @@
  */
 
 #include "ktutil_locl.h"
+#include <heimbase.h>
+#include <base64.h>
 
 RCSID("$Id$");
 
@@ -153,6 +155,178 @@ kt_add(struct add_options *opt, int argc, char **argv)
 	krb5_warn(context, ret, "add");
  out:
     krb5_kt_free_entry(context, &entry);
-    krb5_kt_close(context, keytab);
+    if (ret == 0) {
+        ret = krb5_kt_close(context, keytab);
+        if (ret)
+            krb5_warn(context, ret, "Could not write the keytab");
+    } else {
+        krb5_kt_close(context, keytab);
+    }
+    return ret != 0;
+}
+
+/* We might be reading from a pipe, so we can't use rk_undumpdata() */
+static char *
+read_file(FILE *f)
+{
+    size_t alloced;
+    size_t len = 0;
+    size_t bytes;
+    char *res, *end, *p;
+
+    if ((res = malloc(1024)) == NULL)
+        err(1, "Out of memory");
+    alloced = 1024;
+
+    end = res + alloced;
+    p = res;
+    do {
+        if (p == end) {
+            char *tmp;
+
+            if ((tmp = realloc(res, alloced + (alloced > 1))) == NULL)
+                err(1, "Out of memory");
+            alloced += alloced > 1;
+            p = tmp + (p - res);
+            res = tmp;
+            end = res + alloced;
+        }
+        bytes = fread(p, 1, end - p, f);
+        len += bytes;
+        p += bytes;
+    } while (bytes && !feof(f) && !ferror(f));
+
+    if (ferror(f))
+        errx(1, "Could not read all input");
+    if (p == end) {
+        char *tmp;
+
+        if ((tmp = strndup(res, len)) == NULL)
+            err(1, "Out of memory");
+        free(res);
+        res = tmp;
+    }
+    if (strlen(res) != len)
+        err(1, "Embedded NULs in input!");
+    return res;
+}
+
+static void
+json2keytab_entry(heim_dict_t d, krb5_keytab kt, size_t idx)
+{
+    krb5_keytab_entry e;
+    krb5_error_code ret;
+    heim_object_t v;
+    uint64_t u;
+    int64_t i;
+    char *buf = NULL;
+
+    memset(&e, 0, sizeof(e));
+
+    v = heim_dict_get_value(d, HSTR("timestamp"));
+    if (heim_get_tid(v) != HEIM_TID_NUMBER)
+        goto bad;
+    u = heim_number_get_long(v);
+    e.timestamp = u;
+    if (u != (uint64_t)e.timestamp)
+        goto bad;
+
+    v = heim_dict_get_value(d, HSTR("kvno"));
+    if (heim_get_tid(v) != HEIM_TID_NUMBER)
+        goto bad;
+    i = heim_number_get_long(v);
+    e.vno = i;
+    if (i != (int64_t)e.vno)
+        goto bad;
+
+    v = heim_dict_get_value(d, HSTR("enctype_number"));
+    if (heim_get_tid(v) != HEIM_TID_NUMBER)
+        goto bad;
+    i = heim_number_get_long(v);
+    e.keyblock.keytype = i;
+    if (i != (int64_t)e.keyblock.keytype)
+        goto bad;
+
+    v = heim_dict_get_value(d, HSTR("key"));
+    if (heim_get_tid(v) != HEIM_TID_STRING)
+        goto bad;
+    {
+        const char *s = heim_string_get_utf8(v);
+        int declen;
+
+        if ((buf = malloc(strlen(s))) == NULL)
+            err(1, "Out of memory");
+        declen = rk_base64_decode(s, buf);
+        if (declen < 0)
+            goto bad;
+        e.keyblock.keyvalue.data = buf;
+        e.keyblock.keyvalue.length = declen;
+    }
+
+    v = heim_dict_get_value(d, HSTR("principal"));
+    if (heim_get_tid(v) != HEIM_TID_STRING)
+        goto bad;
+    ret = krb5_parse_name(context, heim_string_get_utf8(v), &e.principal);
+    if (ret == 0)
+        ret = krb5_kt_add_entry(context, kt, &e);
+
+    /* For now, ignore aliases; besides, they're never set anywhere in-tree */
+
+    if (ret)
+        krb5_warn(context, ret,
+                  "Could not parse or write keytab entry %lu",
+                  (unsigned long)idx);
+bad:
+    krb5_free_principal(context, e.principal);
+}
+
+int
+kt_import(void *opt, int argc, char **argv)
+{
+    krb5_error_code ret;
+    krb5_keytab kt;
+    heim_object_t o;
+    heim_error_t json_err = NULL;
+    heim_json_flags_t flags = HEIM_JSON_F_STRICT;
+    FILE *f = argc == 0 ? stdin : fopen(argv[0], "r");
+    size_t alen, i;
+    char *json;
+
+    if (f == NULL)
+        err(1, "Could not open file %s", argv[0]);
+
+    json = read_file(f);
+    o = heim_json_create(json, 10, flags, &json_err);
+    free(json);
+    if (o == NULL) {
+        if (json_err != NULL) {
+            o = heim_error_copy_string(json_err);
+            if (o)
+                errx(1, "Could not parse JSON: %s", heim_string_get_utf8(o));
+        }
+        errx(1, "Could not parse JSON");
+    }
+
+    if (heim_get_tid(o) != HEIM_TID_ARRAY)
+        errx(1, "JSON text must be an array");
+
+    alen = heim_array_get_length(o);
+    if (alen == 0)
+        errx(1, "Empty JSON array; not overwriting keytab");
+
+    if ((kt = ktutil_open_keytab()) == NULL)
+	err(1, "Could not open keytab");
+
+    for (i = 0; i < alen; i++) {
+        heim_object_t e = heim_array_get_value(o, i);
+
+        if (heim_get_tid(e) != HEIM_TID_DICT)
+            warnx("Element %ld of JSON text array is not an object", (long)i);
+        else
+            json2keytab_entry(heim_array_get_value(o, i), kt, i);
+    }
+    ret = krb5_kt_close(context, kt);
+    if (ret)
+        krb5_warn(context, ret, "Could not write the keytab");
     return ret != 0;
 }
diff --git a/admin/copy.c b/admin/copy.c
index 7b50de1c3..8acd6e48e 100644
--- a/admin/copy.c
+++ b/admin/copy.c
@@ -47,7 +47,7 @@ compare_keyblock(const krb5_keyblock *a, const krb5_keyblock *b)
 }
 
 int
-kt_copy (void *opt, int argc, char **argv)
+kt_copy (struct copy_options *opt, int argc, char **argv)
 {
     krb5_error_code ret;
     krb5_keytab src_keytab, dst_keytab;
@@ -106,11 +106,18 @@ kt_copy (void *opt, int argc, char **argv)
 			   "already exists for %s, keytype %s, kvno %d",
 			   name_str, etype_str, entry.vno);
 	    }
-	    krb5_kt_free_entry(context, &dummy);
-	    krb5_kt_free_entry (context, &entry);
-	    free(name_str);
-	    free(etype_str);
-	    continue;
+            if (!opt->copy_duplicates_flag) {
+                krb5_kt_free_entry(context, &dummy);
+                krb5_kt_free_entry (context, &entry);
+                free(name_str);
+                free(etype_str);
+                continue;
+            }
+            /*
+             * Because we can end up trying all keys that match the enctype,
+             * copying entries with duplicate principal, vno, and enctype, but
+             * different keys, can be useful.
+             */
 	} else if(ret != KRB5_KT_NOTFOUND) {
 	    krb5_warn (context, ret, "%s: fetching %s/%s/%u",
 		       to, name_str, etype_str, entry.vno);
diff --git a/admin/get.c b/admin/get.c
index f56e50f43..ecd6f6a16 100644
--- a/admin/get.c
+++ b/admin/get.c
@@ -197,23 +197,27 @@ kt_get(struct get_options *opt, int argc, char **argv)
 		break;
 	}
 
-	ret = kadm5_create_principal(kadm_handle, &princ, mask, "thisIs_aUseless.password123");
-	if(ret == 0)
-	    created = 1;
-	else if(ret != KADM5_DUP) {
-	    krb5_warn(context, ret, "kadm5_create_principal(%s)", argv[a]);
-	    krb5_free_principal(context, princ_ent);
-	    failed++;
-	    continue;
-	}
-        ret = kadm5_randkey_principal_3(kadm_handle, princ_ent, keep, nks, ks,
-                                        &keys, &n_keys);
-	if (ret) {
-	    krb5_warn(context, ret, "kadm5_randkey_principal(%s)", argv[a]);
-	    krb5_free_principal(context, princ_ent);
-	    failed++;
-	    continue;
-	}
+        if (opt->create_flag) {
+            ret = kadm5_create_principal(kadm_handle, &princ, mask, "thisIs_aUseless.password123");
+            if(ret == 0)
+                created = 1;
+            else if(ret != KADM5_DUP) {
+                krb5_warn(context, ret, "kadm5_create_principal(%s)", argv[a]);
+                krb5_free_principal(context, princ_ent);
+                failed++;
+                continue;
+            }
+        }
+        if (opt->change_keys_flag) {
+            ret = kadm5_randkey_principal_3(kadm_handle, princ_ent, keep, nks, ks,
+                                            &keys, &n_keys);
+            if (ret) {
+                krb5_warn(context, ret, "kadm5_randkey_principal(%s)", argv[a]);
+                krb5_free_principal(context, princ_ent);
+                failed++;
+                continue;
+            }
+        }
 
 	ret = kadm5_get_principal(kadm_handle, princ_ent, &princ,
 			      KADM5_PRINCIPAL | KADM5_KVNO | KADM5_ATTRIBUTES);
diff --git a/admin/ktutil-commands.in b/admin/ktutil-commands.in
index 51c8df0be..a85eb5c57 100644
--- a/admin/ktutil-commands.in
+++ b/admin/ktutil-commands.in
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2004 Kungliga Tekniska Högskolan
+ * Copyright (c) 2004-2022 Kungliga Tekniska Högskolan
  * (Royal Institute of Technology, Stockholm, Sweden). 
  * All rights reserved. 
  *
@@ -151,11 +151,17 @@ command = {
 }
 command = {
 	name = "copy"
+	name = "merge"
 	function = "kt_copy"
+	option = {
+		long = "copy-duplicates"
+		type = "flag"
+		help = "copy entries for the same principal and kvno, but different keys"
+	}
 	argument = "source destination"
 	min_args = "2"
 	max_args = "2"
-	help = "Copies one keytab to another."
+	help = "Merges one keytab into another."
 }
 command = {
 	name = "get"
@@ -166,6 +172,16 @@ command = {
 		help = "admin principal"
 		argument = "principal"
 	}
+	option = {
+		long = "create"
+		type = "-flag"
+		help = "do not create the principal"
+	}
+	option = {
+		long = "change-keys"
+		type = "-flag"
+		help = "do not change the principal's keys"
+	}
 	option = {
 		long = "enctypes"
 		short = "e"
@@ -214,6 +230,14 @@ command = {
 	argument = "principal..."
 	help = "Change keys for specified principals, and add them to the keytab."
 }
+command = {
+	name = "import"
+	function = "kt_import"
+	help = "Imports a keytab from JSON output of ktutil list --json --keys."
+        min_args = "0"
+        max_args = "1"
+	argument = "JSON-FILE"
+}
 command = {
 	name = "list"
 	option = {
diff --git a/admin/ktutil.1 b/admin/ktutil.1
index 1a78cc868..0036edcbd 100644
--- a/admin/ktutil.1
+++ b/admin/ktutil.1
@@ -82,29 +82,67 @@ server for the realm of a keytab entry.  Otherwise it will use the
 values specified by the options.
 .Pp
 If no principals are given, all the ones in the keytab are updated.
-.It Nm copy Ar keytab-src Ar keytab-dest
+.It Nm copy Oo Fl Fl copy-duplicates Oc Ar keytab-src Ar keytab-dest
 Copies all the entries from
 .Ar keytab-src
 to
 .Ar keytab-dest .
+Because entries already in
+.Ar keytab-dest
+are kept, this command functions to merge keytabs.
+Entries for the same principal, key version number, and
+encryption type in the
+.Ar keytab-src
+that are also in the
+.Ar keytab-dest
+will not be copied to the
+.Ar keytab-dest
+unless the
+.Fl Fl copy-duplicates
+option is given.
 .It Nm get Oo Fl p Ar admin principal Oc \
 Oo Fl Fl principal= Ns Ar admin principal Oc Oo Fl e Ar enctype Oc \
+Oo Fl Fl no-create Oc \
+Oo Fl Fl no-change-keys Oc \
 Oo Fl Fl keepold | Fl Fl keepallold | Fl Fl pruneall Oc \
 Oo Fl Fl enctypes= Ns Ar enctype Oc Oo Fl r Ar realm Oc \
 Oo Fl Fl realm= Ns Ar realm Oc Oo Fl a Ar admin server Oc \
 Oo Fl Fl admin-server= Ns Ar admin server Oc Oo Fl s Ar server port Oc \
 Oo Fl Fl server-port= Ns Ar server port Oc Ar principal ...
+.Pp
 For each
 .Ar principal ,
-generate a new key for it (creating it if it doesn't already exist),
-and put that key in the keytab.
+get a the principal's keys from the KDC via the kadmin protocol,
+creating the principal if it doesn't exist (unless
+.Fl Fl no-create
+is given), and changing its keys to new random keys (unless
+.Fl Fl no-change-keys
+is given).
 .Pp
 If no
 .Ar realm
 is specified, the realm to operate on is taken from the first
 principal.
+.It Nm import Oo JSON-FILE Oc
+Read an array of keytab entries in a JSON file and copy them to
+the keytab.
+Use the
+.Nm list
+command with its
+.Fl Fl json
+option
+and
+.Fl Fl keys
+option to export a keytab.
 .It Nm list Oo Fl Fl keys Oc Op Fl Fl timestamp Oo Op Fl Fl json Oc
 List the keys stored in the keytab.
+Use the
+.Fl Fl json
+and
+.Fl Fl keys
+options to export a keytab as JSON for importing with the
+.Nm import
+command.
 .It Nm remove Oo Fl p Ar principal Oc Oo Fl Fl principal= Ns Ar principal Oc \
 Oo Fl V kvno Oc Oo Fl Fl kvno= Ns Ar kvno Oc Oo Fl e enctype Oc \
 Oo Fl Fl enctype= Ns Ar enctype Oc
@@ -113,8 +151,14 @@ Removes the specified key or keys. Not specifying a
 removes keys with any version number. Not specifying an
 .Ar enctype
 removes keys of any type.
+.It Nm merge Oo Fl Fl copy-duplicates Oc Ar keytab-src Ar keytab-dest
+An alias for the
+.Nm copy
+command.
 .It Nm rename Ar from-principal Ar to-principal
-Renames all entries in the keytab that match the
+Renames all entries for the
+.Ar from-principal
+in the keytab
 .Ar from-principal
 to
 .Ar to-principal .
@@ -123,6 +167,12 @@ Removes all old versions of a key for which there is a newer version
 that is at least
 .Ar age
 (default one week) old.
+Note that this does not update the KDC database.
+The
+.Xr kadmin 1
+command has a
+.Nm prune
+command that can do this on the KDC side.
 .El
 .Sh SEE ALSO
 .Xr kadmin 1