From 6331812b0f07438f08c28222f0b14f19d1467a3b Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Mon, 10 Jun 2024 23:00:03 +0300 Subject: [PATCH] support authentication indicators in GSSAPI RFC 6680 defines a set of GSSAPI extensions to handle attributes associated with the GSSAPI names. MIT Kerberos and FreeIPA use name attributes to add information about pre-authentication methods used to acquire the initial Kerberos ticket. The attribute 'auth-indicators' may contain list of strings that KDC has associated with the ticket issuance process. Use authentication indicators to authorise or deny access to SSH server. GSSAPIIndicators setting allows to specify a list of possible indicators that a Kerberos ticket presented must or must not contain. More details on the syntax are provided in sshd_config(5) man page. Fixes: https://bugzilla.mindrot.org/show_bug.cgi?id=2696 Signed-off-by: Alexander Bokovoy --- configure.ac | 1 + gss-serv-krb5.c | 74 ++++++++++++++++++++++++++++++++++- gss-serv.c | 101 +++++++++++++++++++++++++++++++++++++++++++++++- readconf.h | 2 + servconf.c | 43 ++++++++++++++++++++- servconf.h | 3 ++ ssh-gss.h | 7 ++++ sshd_config.5 | 47 ++++++++++++++++++++++ 8 files changed, 274 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index 717fef83279..e2644b19416 100644 --- a/configure.ac +++ b/configure.ac @@ -4862,6 +4862,7 @@ AC_ARG_WITH([kerberos5], AC_CHECK_HEADERS([gssapi.h gssapi/gssapi.h]) AC_CHECK_HEADERS([gssapi_krb5.h gssapi/gssapi_krb5.h]) AC_CHECK_HEADERS([gssapi_generic.h gssapi/gssapi_generic.h]) + AC_CHECK_HEADERS([gssapi_ext.h gssapi/gssapi_ext.h]) AC_SEARCH_LIBS([k_hasafs], [kafs], [AC_DEFINE([USE_AFS], [1], [Define this if you want to use libkafs' AFS support])]) diff --git a/gss-serv-krb5.c b/gss-serv-krb5.c index a151bc1e4ad..a94f2a35ba2 100644 --- a/gss-serv-krb5.c +++ b/gss-serv-krb5.c @@ -76,6 +76,52 @@ ssh_gssapi_krb5_init(void) return 1; } +/* Check if any of the indicators in the Kerberos ticket match + * one of indicators in the list of allowed/denied rules. + * In case of the match, apply the decision from the rule. + * If there is no match: + * - if there were both denial and allow rules, deny + * - if there were only denial rules, allow + * - if there were only allow rules, deny + */ + +static int +ssh_gssapi_check_indicators(ssh_gssapi_client *client, int *matched) +{ + char *chptr; + int denial, allows = 0, denials = 0; + u_int i, j; + + /* Check indicators */ + for (i = 0; i < options.num_gss_indicators; i++) { + for (j = 0; client->indicators[j] != NULL; j++) { + chptr = strchr(options.gss_indicators[i], '='); + chptr[0] = '\0'; + if (strcmp(client->indicators[j], + options.gss_indicators[i]) == 0) { + *matched = j; + } + chptr[0] = '='; + denial = (chptr[1] == 'd' || chptr[1] == 'D') ? 1 : 0; + denials |= denial; + allows |= !denial; + if (*matched) { + return (1 - denial); + } + } + } + /* No matches. + * If there were both types of rules, deny */ + if (denials && allows) + return 0; + /* If there are only denials, allow the rest */ + if (denials) { + return 1; + } + /* If there are only allows, deny the rest */ + return 0; +} + /* Check if this user is OK to login. This only works with krb5 - other * GSSAPI mechanisms will need their own. * Returns true if the user is OK to log in, otherwise returns 0 @@ -87,6 +133,7 @@ ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) krb5_principal princ; int retval; const char *errmsg; + int matched; if (ssh_gssapi_krb5_init() == 0) return 0; @@ -100,11 +147,34 @@ ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) } if (krb5_kuserok(krb_context, princ, name)) { retval = 1; - logit("Authorized to %s, krb5 principal %s (krb5_kuserok)", - name, (char *)client->displayname.value); + errmsg = "krb5_kuserok"; } else retval = 0; + if ((retval == 1) && (options.num_gss_indicators != 0)) { + matched = -1; + if (client->indicators) { + retval = ssh_gssapi_check_indicators(client, &matched); + if (matched != -1) { + logit("Ticket contains indicator %s, " + "krb5 principal %s is %s", + client->indicators[matched], + (char *)client->displayname.value, + retval ? "allowed" : "denied"); + goto cont; + } + } + if ((matched == -1) && (retval == 0)) { + logit("GSSAPI authentication indicators enforced " + "but not matched. krb5 principal %s denied", + (char *)client->displayname.value); + } + } +cont: + if (retval == 1) { + logit("Authorized to %s, krb5 principal %s (%s)", + name, (char *)client->displayname.value, errmsg); + } krb5_free_principal(krb_context, princ); return retval; } diff --git a/gss-serv.c b/gss-serv.c index 00e3d118bd1..4ef2dfa6ba0 100644 --- a/gss-serv.c +++ b/gss-serv.c @@ -51,7 +51,7 @@ extern ServerOptions options; static ssh_gssapi_client gssapi_client = { GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, - GSS_C_NO_CREDENTIAL, NULL, {NULL, NULL, NULL, NULL}}; + GSS_C_NO_CREDENTIAL, NULL, {NULL, NULL, NULL, NULL}, NULL}; ssh_gssapi_mech gssapi_null_mech = { NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL}; @@ -268,6 +268,89 @@ ssh_gssapi_parse_ename(Gssctxt *ctx, gss_buffer_t ename, gss_buffer_t name) return GSS_S_COMPLETE; } + +/* Extract authentication indicators from the Kerberos ticket. Authentication + * indicators are GSSAPI name attributes for the name "auth-indicators". + * Multiple indicators might be present in the ticket. + * Each indicator is a utf8 string. */ + +#define AUTH_INDICATORS_TAG "auth-indicators" + +/* Privileged (called from accept_secure_ctx) */ +static OM_uint32 +ssh_gssapi_getindicators(Gssctxt *ctx, gss_name_t gss_name, ssh_gssapi_client *client) +{ + gss_buffer_set_t attrs = GSS_C_NO_BUFFER_SET; + gss_buffer_desc value = GSS_C_EMPTY_BUFFER; + gss_buffer_desc display_value = GSS_C_EMPTY_BUFFER; + int is_mechname, authenticated, complete, more; + size_t count, i; + + ctx->major = gss_inquire_name(&ctx->minor, gss_name, + &is_mechname, NULL, &attrs); + if (ctx->major != GSS_S_COMPLETE) { + return (ctx->major); + } + + if (attrs == GSS_C_NO_BUFFER_SET) { + /* No indicators in the ticket */ + return (0); + } + + count = 0; + for (i = 0; i < attrs->count; i++) { + /* skip anything but auth-indicators */ + if (strncmp(AUTH_INDICATORS_TAG, + attrs->elements[i].value, + sizeof(AUTH_INDICATORS_TAG) - 1) != 0) + continue; + count++; + } + + if (count == 0) { + /* No auth-indicators in the ticket */ + (void) gss_release_buffer_set(&ctx->minor, &attrs); + return (0); + } + + client->indicators = recallocarray(NULL, 0, count + 1, sizeof(char*)); + count = 0; + for (i = 0; i < attrs->count; i++) { + authenticated = 0; + complete = 0; + more = -1; + /* skip anything but auth-indicators */ + if (strncmp(AUTH_INDICATORS_TAG, attrs->elements[i].value, + sizeof(AUTH_INDICATORS_TAG) - 1) != 0) + continue; + /* retrieve all indicators */ + while (more != 0) { + value.value = NULL; + display_value.value = NULL; + ctx->major = gss_get_name_attribute(&ctx->minor, gss_name, + &attrs->elements[i], &authenticated, + &complete, &value, &display_value, &more); + if (ctx->major != GSS_S_COMPLETE) { + goto out; + } + + if ((value.value != NULL) && authenticated) { + client->indicators[count] = xmalloc(value.length + 1); + memcpy(client->indicators[count], value.value, value.length); + client->indicators[count][value.length] = '\0'; + count++; + } + } + } + +out: + (void) gss_release_buffer(&ctx->minor, &value); + (void) gss_release_buffer(&ctx->minor, &display_value); + (void) gss_release_buffer_set(&ctx->minor, &attrs); + return (ctx->major); +} + + /* Extract the client details from a given context. This can only reliably * be called once for a context */ @@ -309,6 +392,13 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_gssapi_client *client) return (ctx->major); } + /* Retrieve authentication indicators, if they exist */ + if ((ctx->major = ssh_gssapi_getindicators(ctx, + ctx->client, client))) { + ssh_gssapi_error(ctx); + return (ctx->major); + } + /* We can't copy this structure, so we just move the pointer to it */ client->creds = ctx->client_creds; ctx->client_creds = GSS_C_NO_CREDENTIAL; @@ -359,6 +449,7 @@ int ssh_gssapi_userok(char *user) { OM_uint32 lmin; + size_t i; if (gssapi_client.exportedname.length == 0 || gssapi_client.exportedname.value == NULL) { @@ -373,6 +464,14 @@ ssh_gssapi_userok(char *user) gss_release_buffer(&lmin, &gssapi_client.displayname); gss_release_buffer(&lmin, &gssapi_client.exportedname); gss_release_cred(&lmin, &gssapi_client.creds); + + if (gssapi_client.indicators != NULL) { + for(i = 0; gssapi_client.indicators[i] != NULL; i++) { + free(gssapi_client.indicators[i]); + } + free(gssapi_client.indicators); + } + explicit_bzero(&gssapi_client, sizeof(ssh_gssapi_client)); return 0; diff --git a/readconf.h b/readconf.h index 9447d5d6e53..4cc94072326 100644 --- a/readconf.h +++ b/readconf.h @@ -41,6 +41,8 @@ typedef struct { int hostbased_authentication; /* ssh2's rhosts_rsa */ int gss_authentication; /* Try GSS authentication */ int gss_deleg_creds; /* Delegate GSS credentials */ + u_int num_gss_indicators; + char **gss_indicators; /* GSSAPI authentication indicators */ int password_authentication; /* Try password * authentication. */ int kbd_interactive_authentication; /* Try keyboard-interactive auth. */ diff --git a/servconf.c b/servconf.c index 8fe56939ced..7164e8cc147 100644 --- a/servconf.c +++ b/servconf.c @@ -134,6 +134,8 @@ initialize_server_options(ServerOptions *options) options->gss_authentication=-1; options->gss_cleanup_creds = -1; options->gss_strict_acceptor = -1; + options->gss_indicators = NULL; + options->num_gss_indicators = 0; options->password_authentication = -1; options->kbd_interactive_authentication = -1; options->permit_empty_passwd = -1; @@ -544,7 +546,7 @@ typedef enum { sHostKeyAlgorithms, sPerSourceMaxStartups, sPerSourceNetBlockSize, sPerSourcePenalties, sPerSourcePenaltyExemptList, sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, - sGssAuthentication, sGssCleanupCreds, sGssStrictAcceptor, + sGssAuthentication, sGssCleanupCreds, sGssStrictAcceptor, sGssIndicators, sAcceptEnv, sSetEnv, sPermitTunnel, sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, sUsePrivilegeSeparation, sAllowAgentForwarding, @@ -629,10 +631,12 @@ static struct { { "gssapiauthentication", sGssAuthentication, SSHCFG_ALL }, { "gssapicleanupcredentials", sGssCleanupCreds, SSHCFG_GLOBAL }, { "gssapistrictacceptorcheck", sGssStrictAcceptor, SSHCFG_GLOBAL }, + { "gssapiindicators", sGssIndicators, SSHCFG_ALL }, #else { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, { "gssapistrictacceptorcheck", sUnsupported, SSHCFG_GLOBAL }, + { "gssapiindicators", sUnsupported, SSHCFG_ALL }, #endif { "passwordauthentication", sPasswordAuthentication, SSHCFG_ALL }, { "kbdinteractiveauthentication", sKbdInteractiveAuthentication, SSHCFG_ALL }, @@ -1568,6 +1572,41 @@ process_server_config_line_depth(ServerOptions *options, char *line, intptr = &options->gss_strict_acceptor; goto parse_flag; + case sGssIndicators: + found = options->num_gss_indicators == 0; + while ((arg = argv_next(&ac, &av)) != NULL) { + if (*arg == '\0') { + error("%s line %d: keyword %s empty argument", + filename, linenum, keyword); + goto out; + } + p = strchr(arg, '='); + if (p == NULL) { + error("%s line %d: Invalid %s, must be name=accept|deny", + filename, linenum, keyword); + goto out; + } + p++; + if (!(*p == 'a' || *p == 'A' || *p == 'd' || *p == 'D')) { + error("%s line %d: Invalid %s, must be name=accept|deny", + filename, linenum, keyword); + goto out; + } + opt_array_append(filename, linenum, keyword, + &strs, &nstrs, arg); + } + if (nstrs == 0) { + fatal("%s line %d: no %s specified", + filename, linenum, keyword); + } + if (found && *activep) { + options->gss_indicators = strs; + options->num_gss_indicators = nstrs; + strs = NULL; /* transferred */ + nstrs = 0; + } + break; + case sPasswordAuthentication: intptr = &options->password_authentication; goto parse_flag; @@ -3141,6 +3180,8 @@ dump_config(ServerOptions *o) #ifdef GSSAPI dump_cfg_fmtint(sGssAuthentication, o->gss_authentication); dump_cfg_fmtint(sGssCleanupCreds, o->gss_cleanup_creds); + dump_cfg_strarray_oneline(sGssIndicators, o->num_gss_indicators, + o->gss_indicators); #endif dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication); dump_cfg_fmtint(sKbdInteractiveAuthentication, diff --git a/servconf.h b/servconf.h index e6d7b75b9a9..6e8fc968e05 100644 --- a/servconf.h +++ b/servconf.h @@ -169,6 +169,8 @@ typedef struct { char **allow_groups; u_int num_deny_groups; char **deny_groups; + u_int num_gss_indicators; + char **gss_indicators; u_int num_subsystems; char **subsystem_name; @@ -297,6 +299,7 @@ TAILQ_HEAD(include_list, include_item); M_CP_STRARRAYOPT(deny_users, num_deny_users); \ M_CP_STRARRAYOPT(allow_groups, num_allow_groups); \ M_CP_STRARRAYOPT(deny_groups, num_deny_groups); \ + M_CP_STRARRAYOPT(gss_indicators, num_gss_indicators); \ M_CP_STRARRAYOPT(accept_env, num_accept_env); \ M_CP_STRARRAYOPT(setenv, num_setenv); \ M_CP_STRARRAYOPT(auth_methods, num_auth_methods); \ diff --git a/ssh-gss.h b/ssh-gss.h index 7b14e74a8e0..7f390e49622 100644 --- a/ssh-gss.h +++ b/ssh-gss.h @@ -34,6 +34,12 @@ #include #endif +#ifdef HAVE_GSSAPI_EXT_H +#include +#elif defined(HAVE_GSSAPI_GSSAPI_EXT_H) +#include +#endif + #ifdef KRB5 # ifndef HEIMDAL # ifdef HAVE_GSSAPI_GENERIC_H @@ -74,6 +80,7 @@ typedef struct { gss_cred_id_t creds; struct ssh_gssapi_mech_struct *mech; ssh_gssapi_ccache store; + char **indicators; /* auth indicators */ } ssh_gssapi_client; typedef struct ssh_gssapi_mech_struct { diff --git a/sshd_config.5 b/sshd_config.5 index aab589e9cab..ddb8bc9958b 100644 --- a/sshd_config.5 +++ b/sshd_config.5 @@ -753,6 +753,53 @@ machine's default store. This facility is provided to assist with operation on multi homed machines. The default is .Cm yes . +.It Cm GSSAPIIndicators +Specifies whether to accept or deny GSSAPI authenticated access if Kerberos +mechanism is used and Kerberos ticket contains a particular set of +authentication indicators. The values can be specified as a pair of +.Cm name=accept|deny . +When +.Cm accept +is used, then presence of the authentication indicator 'name' is required to +allow the access. When +.Cm deny +is used, then the presence of the authentication indicator 'name' will deny +access to the system. Once +.Cm GSSAPIIndicators +is defined, a Kerberos ticket that has indicators but does not match the +policy will get denial. If at least one indicator is configured, whether for +access or denial, tickets without authentication indicators will be explicitly +rejected. +.Pp +By default systems using MIT Kerberos 1.17 or later will not assign any +indicators. SPAKE and PKINIT methods add authentication indicators +to all successful authentications. The SPAKE pre-authentication method is +preferred over an encrypted timestamp pre-authentication when passwords used to +authenticate user principals. Kerberos KDCs built with Heimdal Kerberos +(including Samba AD DC built with Heimdal) do not add authentication indicators +but are able to inquire them. +.Pp +Indicator name is case-sensitive and depends on the configuration of a +particular Kerberos deployment. Indicators available in MIT Kerberos and +FreeIPA environments: +.Pp +.Bl -tag -width XXXX -offset indent -compact +.It Cm hardened +SPAKE pre-authentication in MIT Kerberos and FreeIPA +.It Cm pkinit +smartcard or PKCS11 token-based pre-authentication in MIT Kerberos and FreeIPA +.It Cm radius +pre-authentication based on a RADIUS server in MIT Kerberos and FreeIPA +.It Cm otp +TOTP/HOTP-based two-factor pre-authentication in FreeIPA +.It Cm idp +OAuth2-based pre-authentication in FreeIPA using an external identity provider +and device authorization grant flow +.It Cm passkey +FIDO2-based pre-authentication in FreeIPA, using FIDO2 USB and NFC tokens +.El +.Pp +The default is to not use GSSAPI authentication indicators for access decisions. .It Cm HostbasedAcceptedAlgorithms Specifies the signature algorithms that will be accepted for hostbased authentication as a list of comma-separated patterns.