From 7a0bab858e348485d6fd56af85438fed2473c9f1 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 | 60 ++++++++++++++++++++++++++-- gss-serv.c | 104 +++++++++++++++++++++++++++++++++++++++++++++++- servconf.c | 15 ++++++- servconf.h | 2 + ssh-gss.h | 7 ++++ sshd_config.5 | 44 ++++++++++++++++++++ 7 files changed, 228 insertions(+), 5 deletions(-) diff --git a/configure.ac b/configure.ac index 954de6cafac1..8fa031f827be 100644 --- a/configure.ac +++ b/configure.ac @@ -4882,6 +4882,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 a151bc1e4ad2..229b87543b89 100644 --- a/gss-serv-krb5.c +++ b/gss-serv-krb5.c @@ -41,6 +41,7 @@ #include "log.h" #include "misc.h" #include "servconf.h" +#include "match.h" #include "ssh-gss.h" @@ -76,6 +77,32 @@ 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. + * In case of no indicator from the ticket matching the rule, deny + */ + +static int +ssh_gssapi_check_indicators(ssh_gssapi_client *client, int *matched) +{ + int ret; + u_int i; + + /* Check indicators */ + for (i = 0; client->indicators[i] != NULL; i++) { + ret = match_pattern_list(client->indicators[i], + options.gss_indicators, 1); + /* negative or positive match */ + if (ret != 0) { + *matched = i; + return ret; + } + } + /* No rule matched */ + 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 @@ -85,7 +112,7 @@ static int ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) { krb5_principal princ; - int retval; + int retval, matched; const char *errmsg; if (ssh_gssapi_krb5_init() == 0) @@ -100,11 +127,38 @@ 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.gss_indicators != NULL)) { + /* At this point the configuration enforces presence of indicators + * so we drop the authorization result again */ + retval = 0; + if (client->indicators) { + matched = -1; + retval = ssh_gssapi_check_indicators(client, &matched); + if (retval != 0) { + retval = (retval == 1); + logit("Ticket contains indicator %s, " + "krb5 principal %s is %s", + client->indicators[matched], + (char *)client->displayname.value, + retval ? "allowed" : "denied"); + goto cont; + } + } + if (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 00e3d118bd1f..3edd483833f7 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,92 @@ 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 (((sizeof(AUTH_INDICATORS_TAG) - 1) != attrs->elements[i].length) || + 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 (((sizeof(AUTH_INDICATORS_TAG) - 1) != attrs->elements[i].length) || + 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 +395,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 +452,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 +467,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/servconf.c b/servconf.c index 61d29f5d1391..62ec108c12bc 100644 --- a/servconf.c +++ b/servconf.c @@ -139,6 +139,7 @@ initialize_server_options(ServerOptions *options) options->gss_authentication=-1; options->gss_cleanup_creds = -1; options->gss_strict_acceptor = -1; + options->gss_indicators = NULL; options->password_authentication = -1; options->kbd_interactive_authentication = -1; options->permit_empty_passwd = -1; @@ -557,7 +558,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, @@ -644,10 +645,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 }, @@ -1593,6 +1596,15 @@ process_server_config_line_depth(ServerOptions *options, char *line, intptr = &options->gss_strict_acceptor; goto parse_flag; + case sGssIndicators: + arg = argv_next(&ac, &av); + if (!arg || *arg == '\0') + fatal("%s line %d: %s missing argument.", + filename, linenum, keyword); + if (options->gss_indicators == NULL) + options->gss_indicators = xstrdup(arg); + break; + case sPasswordAuthentication: intptr = &options->password_authentication; goto parse_flag; @@ -3179,6 +3191,7 @@ dump_config(ServerOptions *o) #ifdef GSSAPI dump_cfg_fmtint(sGssAuthentication, o->gss_authentication); dump_cfg_fmtint(sGssCleanupCreds, o->gss_cleanup_creds); + dump_cfg_string(sGssIndicators, o->gss_indicators); #endif dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication); dump_cfg_fmtint(sKbdInteractiveAuthentication, diff --git a/servconf.h b/servconf.h index 22b158d1057c..052b2eb94e02 100644 --- a/servconf.h +++ b/servconf.h @@ -171,6 +171,7 @@ typedef struct { char **allow_groups; u_int num_deny_groups; char **deny_groups; + char *gss_indicators; u_int num_subsystems; char **subsystem_name; @@ -296,6 +297,7 @@ TAILQ_HEAD(include_list, include_item); M_CP_STROPT(routing_domain); \ M_CP_STROPT(permit_user_env_allowlist); \ M_CP_STROPT(pam_service_name); \ + M_CP_STROPT(gss_indicators); \ M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \ M_CP_STRARRAYOPT(allow_users, num_allow_users); \ M_CP_STRARRAYOPT(deny_users, num_deny_users); \ diff --git a/ssh-gss.h b/ssh-gss.h index 7b14e74a8e0b..7f390e49622c 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 587a90334743..6b6befc93dcc 100644 --- a/sshd_config.5 +++ b/sshd_config.5 @@ -753,6 +753,50 @@ 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 comma-separated list +.Cm [!]name1,[!]name2,... . +When indicator's name is prefixed with !, the authentication indicator 'name' +will deny access to the system. Otherwise, one of non-negated authentication +indicators must be present in the Kerberos ticket to allow access. If +.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. However, OpenSSH built against Heimdal Kerberos library is able to +inquire authentication indicators and thus can be used to check for their presence. +.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 or encrypted timestamp pre-authentication mechanisms 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.