From d833ce4cbc3439e33d0cf39e1e4b3f4305a1eb05 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Sun, 14 Nov 2021 16:24:48 -0600 Subject: [PATCH] hdb: Namespace referrals Add a new method for issuing referrals for entire namespaces of hostnames. An alias of the form WELLKNOWN/HOSTBASED-NAMESPACE/service/namespace-fqdn@REALM will cause all requests for host-based principals in the given namespace to be referred to the given realm. --- kadmin/kadmin.1 | 33 +++++-- lib/hdb/common.c | 198 ++++++++++++++++++++++++++++++++++------- tests/kdc/check-kdc.in | 7 ++ tests/kdc/krb5.conf.in | 3 + 4 files changed, 203 insertions(+), 38 deletions(-) diff --git a/kadmin/kadmin.1 b/kadmin/kadmin.1 index 401b6a9f6..b0e852931 100644 --- a/kadmin/kadmin.1 +++ b/kadmin/kadmin.1 @@ -166,13 +166,20 @@ and sub-commands rather than having to edit the KDC's configuration file and having to restart the KDC. .Pp -However, there is currently no way to alias namespaces via HDB -entry aliases. -To issue referrals for entire namespaces use the +There are two methods for issuing referrals for entire namespaces +of hostnames. +An alias of the form +.Ar WELLKNOWN/HOSTBASED-NAMESPACE/service/namespace-fqdn@REALM +(see +.Nm add_namespace +below) will cause all requests for host-based principals in the +given namespace to be referred to the given realm. +Alternatively, the KDC will issue referrals for all host-based +service principals whose hostname component matches a .Ar [domain_realm] -section of the KDC's +entry in the KDC's .Ar krb5.conf -file. +file referring to a different realm. .Ed .Pp .Nm add_namespace @@ -182,18 +189,23 @@ file. .Op Fl Fl max-ticket-life= Ns Ar lifetime .Op Fl Fl max-renewable-life= Ns Ar lifetime .Op Fl Fl attributes= Ns Ar attributes -.Ar principal... +.Ar host-based-principal... .Bd -ragged -offset indent Adds a new namespace of virtual host-based or domain-based principals to the database, whose keys will be automatically derived from base keys stored in the namespace record, and which keys will be rotated automatically. -The namespace names should look like -.Ar hostname@REALM +The namespace names are of the same form as host-based principal +names: +.Ar service/hostname@REALM and these will match all host-based or domain-based service names where hostname component of such a principal ends in the labels of the hostname in the namespace name. .Pp +The service name component may be a wild-card (underscore, +.Ar _ ), +in which case it will match any service. +.Pp For example, .Ar bar.baz.example@BAZ.EXAMPLE will match @@ -223,6 +235,11 @@ The default enctypes is as for the .Nm add command. .Pp +Note that namespaces are stored as principals whose names are of the form +.Ar WELLKNOWN/HOSTBASED-NAMESPACE/service/namespace.fqdn@REALM , +with the +.Ar service +.Pp This command has the following alias: .Nm add_ns . .Ed diff --git a/lib/hdb/common.c b/lib/hdb/common.c index 251eb9b77..596c37ce7 100644 --- a/lib/hdb/common.c +++ b/lib/hdb/common.c @@ -982,11 +982,6 @@ derive_keys(krb5_context context, if (!h_is_namespace && !h->entry.flags.virtual_keys) return 0; h->entry.flags.virtual = 1; - if (h_is_namespace) { - /* Set the entry's principal name */ - free_Principal(h->entry.principal); - ret = copy_Principal(princ, h->entry.principal); - } kr.len = 0; kr.val = 0; @@ -1019,7 +1014,7 @@ derive_keys(krb5_context context, } /* The principal name will be used in key derivation and error messages */ - if (ret == 0 && h_is_namespace) + if (ret == 0) ret = krb5_unparse_name(context, princ, &p); /* Sanity check key rotations, determine current & last kr */ @@ -1153,6 +1148,10 @@ derive_keys(krb5_context context, } /* + * Pick a best kvno for the given principal at the given time. + * + * Implements the [hdb] new_service_key_delay configuration parameter. + * * In order for disparate keytab provisioning systems such as OSKT and our own * kadmin ext_keytab and httpkadmind's get-keys to coexist, we need to be able * to force keys set by the former to not become current keys until users of @@ -1163,9 +1162,9 @@ derive_keys(krb5_context context, * The context is that OSKT's krb5_keytab is very happy to change keys in a way * that requires all members of a cluster to rekey together. If one also * wishes to have cluster members that opt out of this and just fetch current, - * past, and future keys periodically, then the keys set by OSKT need to not - * come into effect until all the opt-out members have had a chance to fetch - * the new keys. + * past, and future keys periodically, then the keys set by OSKT must not come + * into effect until all the opt-out members have had a chance to fetch the new + * keys. * * The assumption is that services will fetch new keys periodically, say, every * four hours. Then one can set `[hdb] new_service_key_delay = 8h' in the @@ -1175,12 +1174,12 @@ derive_keys(krb5_context context, * Naturally, this applies only to concrete principals with concrete keys. */ static krb5_error_code -fix_keys(krb5_context context, - HDB *db, - unsigned flags, - krb5_timestamp now, - krb5uint32 kvno, - hdb_entry_ex *h) +pick_kvno(krb5_context context, + HDB *db, + unsigned flags, + krb5_timestamp now, + krb5uint32 kvno, + hdb_entry_ex *h) { HDB_extension *ext; HDB_Ext_KeySet keys; @@ -1296,7 +1295,7 @@ make_namespace_princ(krb5_context context, /* First go around, need a namespace princ. Make it! */ ret = krb5_build_principal(context, namespace, strlen(realm), - realm, "WELLKNOWN", + realm, KRB5_WELLKNOWN_NAME, HDB_WK_NAMESPACE, comp0, NULL); if (ret == 0) ret = krb5_principal_set_comp_string(context, *namespace, 3, comp1); @@ -1307,6 +1306,138 @@ make_namespace_princ(krb5_context context, return ret; } +static int +is_namespace_princ_p(krb5_context context, + krb5_const_principal princ) +{ + return + krb5_principal_get_num_comp(context, princ) >= 4 + && strcmp(krb5_principal_get_comp_string(context, princ, 0), + KRB5_WELLKNOWN_NAME) == 0 + && strcmp(krb5_principal_get_comp_string(context, princ, 1), + HDB_WK_NAMESPACE) == 0; +} + +/* See call site */ +static krb5_error_code +rewrite_hostname(krb5_context context, + krb5_const_principal wanted_princ, + krb5_const_principal ns_princ, + krb5_const_principal found_ns_princ, + char **s) +{ + const char *ns_host_part, *wanted_host_part, *found_host_part; + const char *p, *r; + size_t ns_host_part_len, wanted_host_part_len; + + wanted_host_part = krb5_principal_get_comp_string(context, wanted_princ, 1); + wanted_host_part_len = strlen(wanted_host_part); + if (wanted_host_part_len > 256) { + krb5_set_error_message(context, HDB_ERR_NOENTRY, + "Aliases of host-based principals longer than " + "256 bytes not supported"); + return HDB_ERR_NOENTRY; + } + + ns_host_part = krb5_principal_get_comp_string(context, ns_princ, 3); + ns_host_part_len = strlen(ns_host_part); + + /* Find `ns_host_part' as the tail of `wanted_host_part' */ + for (r = p = strstr(wanted_host_part, ns_host_part); + r && strnlen(r, ns_host_part_len + 1) > ns_host_part_len; + p = (r = strstr(r, ns_host_part)) ? r : p) + ; + if (!p || strnlen(p, ns_host_part_len + 1) != ns_host_part_len) + return HDB_ERR_NOENTRY; /* Can't happen */ + if (p == wanted_host_part || p[-1] != '.') + return HDB_ERR_NOENTRY; + + found_host_part = + krb5_principal_get_comp_string(context, found_ns_princ, 3); + return + asprintf(s, "%.*s%s", (int)(p - wanted_host_part), wanted_host_part, + found_host_part) < 0 || + *s == NULL ? krb5_enomem(context) : 0; +} + +/* + * Fix `h->entry.principal' to match the desired `princ' in the namespace + * `nsprinc' (which is either the same as `h->entry.principal' or an alias + * of it). + */ +static krb5_error_code +fix_princ_name(krb5_context context, + krb5_const_principal princ, + krb5_const_principal nsprinc, + hdb_entry_ex *h) +{ + krb5_error_code ret = 0; + char *s = NULL; + + if (!nsprinc) + return 0; + if (krb5_principal_get_num_comp(context, princ) < 2) + return HDB_ERR_NOENTRY; + + /* `nsprinc' must be a namespace principal */ + + if (krb5_principal_compare(context, nsprinc, h->entry.principal)) { + /* + * `h' is the HDB entry for `nsprinc', and `nsprinc' is its canonical + * name. + * + * Set the entry's principal name to the desired name. The keys will + * be fixed next (upstairs, but don't forget to!). + */ + free_Principal(h->entry.principal); + return copy_Principal(princ, h->entry.principal); + } + + if (!is_namespace_princ_p(context, h->entry.principal)) { + /* + * The alias is a namespace, but the canonical name is not. WAT. + * + * Well, the KDC will just issue a referral anyways, so we can leave + * `h->entry.principal' as is... + * + * Remove all of `h->entry's keys just in case, and leave + * `h->entry.principal' as-is. + */ + free_Keys(&h->entry.keys); + (void) hdb_entry_clear_password(context, &h->entry); + return hdb_clear_extension(context, &h->entry, + choice_HDB_extension_data_hist_keys); + } + + /* + * A namespace alias of a namespace entry. + * + * We'll want to rewrite the original principal accordingly. + * + * E.g., if the caller wanted host/foo.ns.test.h5l.se and we + * found WELLKNOWN/HOSTBASED-NAMESPACE/ns.test.h5l.se is an + * alias of WELLKNOWN/HOSTBASED-NAMESPACE/ns.example.org, then + * we'll want to treat host/foo.ns.test.h5l.se as an alias of + * host/foo.ns.example.org. + */ + if (krb5_principal_get_num_comp(context, h->entry.principal) != + 2 + krb5_principal_get_num_comp(context, princ)) + ret = HDB_ERR_NOENTRY; /* Only host-based services for now */ + if (ret == 0) + ret = rewrite_hostname(context, princ, nsprinc, h->entry.principal, &s); + if (ret == 0) { + krb5_free_principal(context, h->entry.principal); + h->entry.principal = NULL; + ret = krb5_make_principal(context, &h->entry.principal, + krb5_principal_get_realm(context, princ), + krb5_principal_get_comp_string(context, + princ, 0), + s, + NULL); + } + return ret; +} + /* Wrapper around db->hdb_fetch_kvno() that implements virtual princs/keys */ static krb5_error_code fetch_it(krb5_context context, @@ -1319,7 +1450,7 @@ fetch_it(krb5_context context, hdb_entry_ex *ent) { krb5_const_principal tmpprinc = princ; - krb5_principal baseprinc = NULL; + krb5_principal nsprinc = NULL; krb5_error_code ret = 0; const char *comp0 = krb5_principal_get_comp_string(context, princ, 0); const char *comp1 = krb5_principal_get_comp_string(context, princ, 1); @@ -1331,7 +1462,7 @@ fetch_it(krb5_context context, int do_search = 0; if (db->enable_virtual_hostbased_princs && comp1 && - strcmp("krbtgt", comp0) != 0 && strcmp("WELLKNOWN", comp0) != 0) { + strcmp("krbtgt", comp0) != 0 && strcmp(KRB5_WELLKNOWN_NAME, comp0) != 0) { char *htmp; if ((host = strdup(comp1)) == NULL) @@ -1358,7 +1489,7 @@ fetch_it(krb5_context context, } tmp = host ? host : comp1; - for (ret = HDB_ERR_NOENTRY; ret == HDB_ERR_NOENTRY; tmpprinc = baseprinc) { + for (ret = HDB_ERR_NOENTRY; ret == HDB_ERR_NOENTRY; tmpprinc = nsprinc) { krb5_error_code ret2 = 0; /* @@ -1376,7 +1507,7 @@ fetch_it(krb5_context context, ret = db->hdb_fetch_kvno(context, db, tmpprinc, flags, kvno, ent); if (ret != HDB_ERR_NOENTRY || hdots == 0 || hdots < mindots || !tmp || !do_search) - break; + break; /* * Breadcrumb: @@ -1403,12 +1534,13 @@ fetch_it(krb5_context context, hdots--; } - if (baseprinc == NULL) + if (nsprinc == NULL) /* First go around, need a namespace princ. Make it! */ - ret2 = make_namespace_princ(context, db, tmpprinc, &baseprinc); - /* Update the hostname component */ + ret2 = make_namespace_princ(context, db, tmpprinc, &nsprinc); + + /* Update the hostname component of the namespace principal */ if (ret2 == 0) - ret2 = krb5_principal_set_comp_string(context, baseprinc, 3, tmp); + ret2 = krb5_principal_set_comp_string(context, nsprinc, 3, tmp); if (ret2) ret = ret2; @@ -1425,14 +1557,20 @@ fetch_it(krb5_context context, * key derivation to do, but that's decided in derive_keys(). */ if (ret == 0) { - ret = derive_keys(context, flags, princ, !!baseprinc, t, etype, kvno, - ent); + /* Fix the principal name if namespaced */ + ret = fix_princ_name(context, princ, nsprinc, ent); + + /* Derive keys if namespaced or virtual */ if (ret == 0) - ret = fix_keys(context, db, flags, t, kvno, ent); - if (ret) - hdb_free_entry(context, ent); + ret = derive_keys(context, flags, princ, !!nsprinc, t, etype, kvno, + ent); + /* Pick the best kvno for this principal at the given time */ + if (ret == 0) + ret = pick_kvno(context, db, flags, t, kvno, ent); } - krb5_free_principal(context, baseprinc); + if (ret) + hdb_free_entry(context, ent); + krb5_free_principal(context, nsprinc); free(host); return ret; } diff --git a/tests/kdc/check-kdc.in b/tests/kdc/check-kdc.in index 75626f6ce..e346d71b5 100644 --- a/tests/kdc/check-kdc.in +++ b/tests/kdc/check-kdc.in @@ -78,6 +78,8 @@ server=host/datan.test.h5l.se server2=host/computer.example.com server3=host/refer-me-out.test.h5l.se server4=host/no-auth-data-reqd.test.h5l.se +server5=host/a-host.refer-all-out.test.h5l.se +namespace=WELLKNOWN/HOSTBASED-NAMESPACE/_/refer-all-out.test.h5l.se serverip=host/10.11.12.13 serveripname=host/ip.test.h5l.org serveripname2=host/10.11.12.14 @@ -240,6 +242,9 @@ ${kadmin} add -p foo --use-defaults referral-placeholder@${R5} || exit 1 ${kadmin} add_alias referral-placeholder@${R5} ${server3}@${R} || exit 1 ${kadmin5} add -p kaka --use-defaults ${server3}@${R5} || exit 1 ${kadmin5} ext -k ${keytab} ${server3}@${R5} || exit 1 +${kadmin} add_alias referral-placeholder@${R5} ${namespace}@${R} || exit 1 +${kadmin5} add -p kaka --use-defaults ${server5}@${R5} || exit 1 +${kadmin5} ext -k ${keytab} ${server5}@${R5} || exit 1 ${kadmin} add -p kaka --use-defaults ${serverip}@${R} || exit 1 ${kadmin} ext -k ${keytab} ${serverip}@${R} || exit 1 ${kadmin} add -p kaka --use-defaults ${serveripname}@${R} || exit 1 @@ -444,6 +449,8 @@ echo "Getting x-realm tickets with capaths for $R -> $R5" ${kgetcred} foo@${R5} || { ec=1 ; eval "${testfailed}"; } echo "Testing HDB referral entry" ${kgetcred} --canonicalize ${server3}@${R} || { ec=1 ; eval "${testfailed}"; } +echo "Testing HDB namespace referral entry" +${kgetcred} --canonicalize ${server5}@${R} || { ec=1 ; eval "${testfailed}"; } ${klist} ${kdestroy} diff --git a/tests/kdc/krb5.conf.in b/tests/kdc/krb5.conf.in index 19b4e3ef6..a85836d76 100644 --- a/tests/kdc/krb5.conf.in +++ b/tests/kdc/krb5.conf.in @@ -126,6 +126,9 @@ [hdb] db-dir = @objdir@ + enable_virtual_hostbased_princs = true + virtual_hostbased_princ_mindots = 1 + virtual_hostbased_princ_maxdots = 3 [logging] kdc = 0-/FILE:@objdir@/@messages@.log