From a5273d18cd33c1908ccd49548d7c5010820679d3 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 25 Apr 2022 17:39:29 -0500 Subject: [PATCH] httpkadmind: Support ok-as-delegate and such Add support for configuring the attributes of new principals created via httpkadmind. This can be done via virtual host-based service namespaces, which will provide default attributes even if disabled (but the created principals will not be disabled, naturally), or via krb5.conf. --- kdc/httpkadmind.8 | 84 +++++++++++++++++++++- kdc/httpkadmind.c | 157 +++++++++++++++++++++++++++++++++++++++-- tests/kdc/check-httpkadmind.in | 33 +++++++-- 3 files changed, 261 insertions(+), 13 deletions(-) diff --git a/kdc/httpkadmind.8 b/kdc/httpkadmind.8 index 90b4f63aa..e191ac445 100644 --- a/kdc/httpkadmind.8 +++ b/kdc/httpkadmind.8 @@ -140,6 +140,11 @@ If the named principal(s) is (are) virtual, this will cause it .It Ar create=true If the named principal(s) does not (do not) exist, this will cause it (them) to be created. +The default attributes for new principals created this way will +be taken from any containing virtual host-based service principal +namespace (not including the disabled attribute), or from +.Nm krb5.conf(5) +(see the CONFIGURATION section). .It Ar rotate=true This will cause the keys of concrete principals to be rotated. .It Ar revoke=true @@ -150,6 +155,31 @@ the target will not be able to be decrypted by the caller as it will not have the necessary keys. .El .Pp +The HTTP +.Nm Cache-Control +header will be set on +.Nm get-keys +responses to +.Dq Nm no-store , +and the +.Nm max-age +cache control parameter will be set to the least number of +seconds until before any of the requested principal's keys could +change. +For virtual principals this will be either the time left until a +quarter of the rotation period before the next rotation, or the +time left until a +quarter of the rotation period after the next rotation. +For concrete principals this will be the time left to the first +such principal's password expiration, or, if none of them have a +configured password expiration time, then half of the +.Nm new_service_key_delay +configured in the +.Nm [hdb] +section of the +.Nm krb5.conf(5) +file. +.Pp Authorization is handled via the same mechanism as in .Nm bx509d(8) which was originally intended to authorize certification requests @@ -160,9 +190,10 @@ but using .Nm [ext_keytab] as the .Nm krb5.conf(5) section. -Clients with host-based principals for the the host service can -create and extract keys for their own service name, but otherwise -a number of service names are not denied: +Clients with host-based principals for the +.Dq host +service can create and extract keys for their own service name, +but otherwise a number of service names are denied: .Bl -tag -width Ds -offset indent .It Dq host .It Dq root @@ -361,11 +392,58 @@ Authorizer configuration goes in in .Nm krb5.conf(5). For example: .Pp +.Bd -literal -offset indent [ext_keytab] simple_csr_authorizer_directory = /etc/krb5/simple_csr_authz ipc_csr_authorizer = { service = UNIX:/var/heimdal/csr_authorizer_sock } +.Ed +.Pp +Configuration parameters specific to +.Nm httpkadmind : +.Bl -tag -width Ds -offset indent +.It csr_authorizer_handles_svc_names = BOOL +.It new_hostbased_service_principal_attributes = ... +.El +.Pp +The +.Nm [ext_keytab] +.Nm new_hostbased_service_principal_attributes +parameter may be used instead of virtual host-based service +namespace principals to specify the attributes of new principals +created by +.Nm httpkadmind , +and its value is a hive with a service name then a hostname or +namespace, and whose value is a set of attributes as given in the +.Nm kadmin(1) modify +command. +For example: +.Bd -literal -offset indent +[ext_keytab] + new_hostbased_service_principal_attributes = { + host = { + a-particular-hostname.test.h5l.se = ok-as-delegate + .prod.test.h5l.se = ok-as-delegate + } + } +.Ed +.Pp +which means that +.Dq host/a-particular-hostname.test.h5l.se , +if created via +.Nm httpkadmind , +will be allowed to get delegated credentials (ticket forwarding), +and that hostnames matching the glob pattern +.Dq host/*.prod.test.h5l.se , +if created via +.Nm httpkadmind , +will also allowed to get delegated credentials. +All host-based service principals created via +.Nm httpkadmind +not matchining any +.Nm new_hostbased_service_principal_attributes +service namespaces will have the empty attribute set. .Sh EXAMPLES To start .Nm httpkadmind diff --git a/kdc/httpkadmind.c b/kdc/httpkadmind.c index 0e31b4044..a8a3d1b5e 100644 --- a/kdc/httpkadmind.c +++ b/kdc/httpkadmind.c @@ -177,6 +177,7 @@ typedef struct kadmin_request_desc { char *freeme1; char *enctypes; const char *method; + krb5_timestamp pw_end; unsigned int response_set:1; unsigned int materialize:1; unsigned int rotate_now:1; @@ -657,8 +658,36 @@ resp(kadmin_request_desc r, rmmode); if (response == NULL) return -1; - mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, - "no-store, max-age=0"); + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_AGE, "0"); + if (mret == MHD_YES && http_status_code == MHD_HTTP_OK) { + static HEIMDAL_THREAD_LOCAL char *cache_control = NULL; + krb5_timestamp now; + + free(cache_control); + cache_control = NULL; + krb5_timeofday(r->context, &now); + if (r->pw_end && r->pw_end > now) { + if (asprintf(&cache_control, "no-store, max-age=%lld", + (long long)r->pw_end - now) == -1 || + cache_control == NULL) + /* Soft handling of ENOMEM here */ + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=3600"); + else + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + cache_control); + + } else + mret = MHD_add_response_header(response, + MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=0"); + } else { + /* Shouldn't happen */ + mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL, + "no-store, max-age=0"); + } if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) { size_t i; @@ -1215,6 +1244,93 @@ make_kstuple(krb5_context context, return *kstuple ? 0 :krb5_enomem(context); } +/* Copied from kadmin/util.c */ +struct units kdb_attrs[] = { + { "no-auth-data-reqd", KRB5_KDB_NO_AUTH_DATA_REQUIRED }, + { "disallow-client", KRB5_KDB_DISALLOW_CLIENT }, + { "virtual", KRB5_KDB_VIRTUAL }, + { "virtual-keys", KRB5_KDB_VIRTUAL_KEYS }, + { "allow-digest", KRB5_KDB_ALLOW_DIGEST }, + { "allow-kerberos4", KRB5_KDB_ALLOW_KERBEROS4 }, + { "trusted-for-delegation", KRB5_KDB_TRUSTED_FOR_DELEGATION }, + { "ok-as-delegate", KRB5_KDB_OK_AS_DELEGATE }, + { "new-princ", KRB5_KDB_NEW_PRINC }, + { "support-desmd5", KRB5_KDB_SUPPORT_DESMD5 }, + { "pwchange-service", KRB5_KDB_PWCHANGE_SERVICE }, + { "disallow-svr", KRB5_KDB_DISALLOW_SVR }, + { "requires-pw-change", KRB5_KDB_REQUIRES_PWCHANGE }, + { "requires-hw-auth", KRB5_KDB_REQUIRES_HW_AUTH }, + { "requires-pre-auth", KRB5_KDB_REQUIRES_PRE_AUTH }, + { "disallow-all-tix", KRB5_KDB_DISALLOW_ALL_TIX }, + { "disallow-dup-skey", KRB5_KDB_DISALLOW_DUP_SKEY }, + { "disallow-proxiable", KRB5_KDB_DISALLOW_PROXIABLE }, + { "disallow-renewable", KRB5_KDB_DISALLOW_RENEWABLE }, + { "disallow-tgt-based", KRB5_KDB_DISALLOW_TGT_BASED }, + { "disallow-forwardable", KRB5_KDB_DISALLOW_FORWARDABLE }, + { "disallow-postdated", KRB5_KDB_DISALLOW_POSTDATED }, + { NULL, 0 } +}; + +/* + * Determine the default/allowed attributes for some new principal. + */ +static krb5_flags +create_attributes(kadmin_request_desc r, krb5_const_principal p) +{ + krb5_error_code ret; + const char *srealm = krb5_principal_get_realm(r->context, p); + const char *svc; + const char *hn; + + /* Has to be a host-based service principal (for now) */ + if (krb5_principal_get_num_comp(r->context, p) != 2) + return 0; + + hn = krb5_principal_get_comp_string(r->context, p, 1); + svc = krb5_principal_get_comp_string(r->context, p, 0); + + while (hn && strchr(hn, '.') != NULL) { + kadm5_principal_ent_rec nsprinc; + krb5_principal nsp; + uint64_t a = 0; + const char *as; + + /* Try finding a virtual host-based service principal namespace */ + memset(&nsprinc, 0, sizeof(nsprinc)); + ret = krb5_make_principal(r->context, &nsp, srealm, + KRB5_WELLKNOWN_NAME, HDB_WK_NAMESPACE, + svc, hn, NULL); + if (ret == 0) + ret = kadm5_get_principal(r->kadm_handle, nsp, &nsprinc, + KADM5_PRINCIPAL | KADM5_ATTRIBUTES); + krb5_free_principal(r->context, nsp); + if (ret == 0) { + /* Found one; use it even if disabled, but drop that attribute */ + a = nsprinc.attributes & ~KRB5_KDB_DISALLOW_ALL_TIX; + kadm5_free_principal_ent(r->kadm_handle, &nsprinc); + return a; + } + + /* Fallback on krb5.conf */ + as = krb5_config_get_string(r->context, NULL, "ext_keytab", + "new_hostbased_service_principal_attributes", + svc, hn, NULL); + if (as) { + a = parse_flags(as, kdb_attrs, 0); + if (a == (uint64_t)-1) { + krb5_warnx(r->context, "Invalid value for [ext_keytab] " + "new_hostbased_service_principal_attributes"); + return 0; + } + return a; + } + + hn = strchr(hn + 1, '.'); + } + + return 0; +} + /* * Get keys for one principal. * @@ -1229,7 +1345,8 @@ get_keys1(kadmin_request_desc r, const char *pname) krb5_principal p = NULL; uint32_t mask = KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE | KADM5_MAX_RLIFE | - KADM5_ATTRIBUTES | KADM5_KEY_DATA | KADM5_TL_DATA; + KADM5_PW_EXPIRATION | KADM5_ATTRIBUTES | KADM5_KEY_DATA | + KADM5_TL_DATA; uint32_t create_mask = mask & ~(KADM5_KEY_DATA | KADM5_TL_DATA); size_t nkstuple = 0; int change = 0; @@ -1270,6 +1387,9 @@ get_keys1(kadmin_request_desc r, const char *pname) if (ret == KADM5_UNK_PRINC && r->create) { char pw[128]; + memset(&princ, 0, sizeof(princ)); + princ.attributes = create_attributes(r, p); + if (read_only) ret = KADM5_READ_ONLY; else @@ -1281,7 +1401,6 @@ get_keys1(kadmin_request_desc r, const char *pname) ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */, &r->kadm_handle); } - memset(&princ, 0, sizeof(princ)); /* * Some software is allergic to kvno 1, assuming that kvno 1 implies * half-baked service principal. We've some vague recollection of @@ -1384,6 +1503,36 @@ get_keys1(kadmin_request_desc r, const char *pname) if (ret == 0) ret = write_keytab(r, &princ, pname); + + if (ret == 0) { + /* + * We will use the principal's password expiration to work out the + * value for the max-age Cache-Control. + * + * Virtual service principals will have their `pw_expiration' set to a + * time when the client should refetch keys. + * + * Concrete service principals will generally not have a non-zero + * `pw_expiration', but if we have a new_service_key_delay, then we'll + * use half of it as the max-age Cache-Control. + */ + if (princ.pw_expiration == 0) { + krb5_timestamp nskd = + krb5_config_get_time_default(r->context, NULL, 0, "hdb", + "new_service_key_delay", NULL); + if (nskd) + princ.pw_expiration = time(NULL) + (nskd >> 1); + } + + /* + * This service can be used to fetch more than one principal's keys, so + * the max-age Cache-Control should be derived from the soonest- + * "expiring" principal. + */ + if (r->pw_end == 0 || + (princ.pw_expiration < r->pw_end && princ.pw_expiration > time(NULL))) + r->pw_end = princ.pw_expiration; + } if (freeit) kadm5_free_principal_ent(r->kadm_handle, &princ); krb5_free_principal(r->context, p); diff --git a/tests/kdc/check-httpkadmind.in b/tests/kdc/check-httpkadmind.in index f57f2af85..063ccdf4d 100644 --- a/tests/kdc/check-httpkadmind.in +++ b/tests/kdc/check-httpkadmind.in @@ -133,9 +133,11 @@ fi # HTTP curl-opts HTTP() { - curl -g --resolve ${server}:${restport2}:127.0.0.1 \ - --resolve ${server}:${restport}:127.0.0.1 \ - -u: --negotiate $verbose "$@" + curl -g --resolve ${server}:${restport2}:127.0.0.1 \ + --resolve ${server}:${restport}:127.0.0.1 \ + -u: --negotiate $verbose \ + -D response-headers \ + "$@" } # get_config QPARAMS curl-opts @@ -145,6 +147,23 @@ get_config() { HTTP $verbose "$@" "$url" } +check_age() { + set -- $(grep -i ^Cache-Control: response-headers) + if [ $# -eq 0 ]; then + return 1 + fi + shift + for param in "$@"; do + case "$param" in + no-store) true;; + max-age=0) return 1;; + max-age=*) true;; + *) return 1;; + esac + done + return 0; +} + # get_keytab QPARAMS curl-opts get_keytab() { url="http://${server}:${restport}/get-keys?$1" @@ -163,9 +182,9 @@ get_keytab_POST() { get_keytab "$q" -X POST --data-binary @/dev/null -f "$@" && { echo "POST succeeded w/o CSRF token!"; return 1; } - get_keytab "$q" -X POST --data-binary @/dev/null -D response-headers "$@" + get_keytab "$q" -X POST --data-binary @/dev/null "$@" grep ^X-CSRF-Token: response-headers >/dev/null || return 1 - get_keytab "$q" -X POST --data-binary @/dev/null -D response-headers \ + get_keytab "$q" -X POST --data-binary @/dev/null \ -H "$(sed -e 's/\r//' response-headers | grep ^X-CSRF-Token:)" "$@" grep '^HTTP/1.1 200' response-headers >/dev/null || return $? return 0 @@ -174,7 +193,7 @@ get_keytab_POST() { get_keytab_POST_redir() { url="http://${server}:${restport}/get-keys?$1" shift - HTTP -X POST --data-binary @/dev/null -D response-headers "$@" "$url" + HTTP -X POST --data-binary @/dev/null "$@" "$url" grep ^X-CSRF-Token: response-headers >/dev/null || { echo "POST w/o CSRF token had response w/o CSRF token!"; return 1; } HTTP -X POST --data-binary @/dev/null -f \ @@ -292,6 +311,8 @@ ${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin { echo "Failed to list keytab for $p"; exit 1; } get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || { echo "Failed to get a keytab for $p with curl"; exit 1; } +check_age +grep -i ^Cache-Control response-headers ${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || { echo "Failed to list keytab for $p"; exit 1; } cmp extracted_keytab.kadmin extracted_keytab.rest || -- 2.11.4.GIT