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.
This commit is contained in:
Nicolas Williams
2022-04-25 17:39:29 -05:00
parent cd2e423d10
commit a5273d18cd
3 changed files with 261 additions and 13 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 ||