diff --git a/configure.ac b/configure.ac index 60d45716bde..9e77a306058 100644 --- a/configure.ac +++ b/configure.ac @@ -5035,6 +5035,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..77cfeb05f61 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,34 @@ 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; + + *matched = -1; + + /* 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,26 +114,71 @@ static int ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) { krb5_principal princ; - int retval; + int retval, matched, success; const char *errmsg; if (ssh_gssapi_krb5_init() == 0) return 0; - if ((retval = krb5_parse_name(krb_context, client->exportedname.value, - &princ))) { + retval = krb5_parse_name(krb_context, client->exportedname.value, &princ); + if (retval) { errmsg = krb5_get_error_message(krb_context, retval); logit("krb5_parse_name(): %.100s", errmsg); krb5_free_error_message(krb_context, errmsg); return 0; } - if (krb5_kuserok(krb_context, princ, name)) { + + /* Let the Kerberos library to map the principal to POSIX account */ + success = krb5_kuserok(krb_context, princ, name); + if (success == FALSE) { + retval = 0; + goto out; + } + + /* At this point we are good if no indicators were defined */ + if (options.gss_indicators == NULL) { retval = 1; - logit("Authorized to %s, krb5 principal %s (krb5_kuserok)", - name, (char *)client->displayname.value); - } else + goto out; + } + + /* At this point we have indicators defined in the configuration, + * if clientt did not provide any indicators, we reject */ + if (!client->indicators) { + retval = 0; + logit("GSSAPI authentication indicators enforced " + "but indicators not provided by the client. " + "krb5 principal %s denied", + (char *)client->displayname.value); + goto out; + } + + /* At this point the configuration enforces presence of indicators + * check the match */ + matched = -1; + success = ssh_gssapi_check_indicators(client, &matched); + + switch (success) { + case 1: + logit("Provided indicator %s allowed by the configuration", + client->indicators[matched]); + retval = 1; + break; + case -1: + logit("Provided indicator %s rejected by the configuration", + client->indicators[matched]); + retval = 0; + break; + default: + logit("Provided indicators do not match the configuration"); retval = 0; + break; + } +out: + if (retval == 1) { + logit("Authorized to %s, krb5 principal %s (krb5_userok)", + name, (char *)client->displayname.value); + } krb5_free_principal(krb_context, princ); return retval; } diff --git a/gss-serv.c b/gss-serv.c index 05c347ea058..5fb4fd3a3e7 100644 --- a/gss-serv.c +++ b/gss-serv.c @@ -52,7 +52,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}; @@ -269,6 +269,78 @@ 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 an 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); + } + + client->indicators = NULL; + 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) || + memcmp(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 = xrecallocarray(client->indicators, count, count + 1, sizeof(char*)); + if (client->indicators == NULL) { + fatal("ssh_gssapi_getindicators failed to allocate memory"); + } + 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 */ @@ -310,6 +382,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; @@ -334,7 +413,7 @@ ssh_gssapi_storecreds(void) { if (options.gss_deleg_creds == 0) { debug_f("delegate credential is disabled, doing nothing"); - return 0; + return; } if (gssapi_client.mech && gssapi_client.mech->storecreds) { @@ -365,6 +444,7 @@ int ssh_gssapi_userok(char *user) { OM_uint32 lmin; + size_t i; if (gssapi_client.exportedname.length == 0 || gssapi_client.exportedname.value == NULL) { @@ -379,8 +459,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); - explicit_bzero(&gssapi_client, - sizeof(ssh_gssapi_client)); + + 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; } else diff --git a/servconf.c b/servconf.c index 1b8cfa4b62e..2269184e3df 100644 --- a/servconf.c +++ b/servconf.c @@ -139,6 +139,7 @@ initialize_server_options(ServerOptions *options) options->gss_cleanup_creds = -1; options->gss_deleg_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; @@ -531,6 +532,7 @@ fill_default_server_options(ServerOptions *options) CLEAR_ON_NONE(options->routing_domain); CLEAR_ON_NONE(options->host_key_agent); CLEAR_ON_NONE(options->per_source_penalty_exempt); + CLEAR_ON_NONE(options->gss_indicators); for (i = 0; i < options->num_host_key_files; i++) CLEAR_ON_NONE(options->host_key_files[i]); @@ -568,6 +570,7 @@ typedef enum { sPerSourcePenalties, sPerSourcePenaltyExemptList, sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, sGssAuthentication, sGssCleanupCreds, sGssDelegateCreds, sGssStrictAcceptor, + sGssIndicators, sAcceptEnv, sSetEnv, sPermitTunnel, sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, sUsePrivilegeSeparation, sAllowAgentForwarding, @@ -655,11 +658,13 @@ static struct { { "gssapicleanupcredentials", sGssCleanupCreds, SSHCFG_GLOBAL }, { "gssapidelegatecredentials", sGssDelegateCreds, SSHCFG_GLOBAL }, { "gssapistrictacceptorcheck", sGssStrictAcceptor, SSHCFG_GLOBAL }, + { "gssapiindicators", sGssIndicators, SSHCFG_ALL }, #else { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, { "gssapidelegatecredentials", sUnsupported, SSHCFG_GLOBAL }, { "gssapistrictacceptorcheck", sUnsupported, SSHCFG_GLOBAL }, + { "gssapiindicators", sUnsupported, SSHCFG_ALL }, #endif { "passwordauthentication", sPasswordAuthentication, SSHCFG_ALL }, { "kbdinteractiveauthentication", sKbdInteractiveAuthentication, SSHCFG_ALL }, @@ -1665,6 +1670,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; @@ -3286,6 +3300,7 @@ dump_config(ServerOptions *o) dump_cfg_fmtint(sGssCleanupCreds, o->gss_cleanup_creds); dump_cfg_fmtint(sGssDelegateCreds, o->gss_deleg_creds); dump_cfg_fmtint(sGssStrictAcceptor, o->gss_strict_acceptor); + 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 73d952f095c..0700ddbeadc 100644 --- a/servconf.h +++ b/servconf.h @@ -174,6 +174,7 @@ typedef struct { char **allow_groups; u_int num_deny_groups; char **deny_groups; + char *gss_indicators; u_int num_subsystems; char **subsystem_name; @@ -303,6 +304,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 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 8108dc2daff..516ecd73ecc 100644 --- a/sshd_config.5 +++ b/sshd_config.5 @@ -764,6 +764,52 @@ 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 +.Dq none +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.