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:
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -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 ||
|
||||
|
Reference in New Issue
Block a user