From 5112fd1dbb11740fdccd204bf34e5a168057dc11 Mon Sep 17 00:00:00 2001 From: Zoltan Fridrich Date: Thu, 12 Mar 2026 12:52:26 +0100 Subject: [PATCH] Fix GSSAPI authentication indicator issues found by AI Resolves: RHEL-154309 Signed-off-by: Zoltan Fridrich --- openssh-9.9p1-gssapi-s4u.patch | 1055 ++++++++--------- ...h-9.9p1-sshd-no-delegate-credentials.patch | 51 +- ...-authentication-indicators-in-GSSAPI.patch | 463 ++++---- openssh.spec | 2 + 4 files changed, 781 insertions(+), 790 deletions(-) diff --git a/openssh-9.9p1-gssapi-s4u.patch b/openssh-9.9p1-gssapi-s4u.patch index 7e94f6b..31bf5d5 100644 --- a/openssh-9.9p1-gssapi-s4u.patch +++ b/openssh-9.9p1-gssapi-s4u.patch @@ -1,8 +1,7 @@ -diff --git a/auth-krb5.c b/auth-krb5.c -index a37be93c3..34f058967 100644 ---- a/auth-krb5.c -+++ b/auth-krb5.c -@@ -465,6 +465,19 @@ ssh_krb5_cc_new_unique(krb5_context ctx, krb5_ccache *ccache, int *need_environm +diff --color -ruNp a/auth-krb5.c b/auth-krb5.c +--- a/auth-krb5.c 2026-03-12 11:51:16.406723629 +0100 ++++ b/auth-krb5.c 2026-03-12 12:00:49.464289202 +0100 +@@ -462,6 +462,19 @@ ssh_krb5_cc_new_unique(krb5_context ctx, * a primary cache for this collection, if it supports that (non-FILE) */ if (krb5_cc_support_switch(ctx, type)) { @@ -22,21 +21,19 @@ index a37be93c3..34f058967 100644 debug3_f("calling cc_new_unique(%s)", ccname); ret = krb5_cc_new_unique(ctx, type, NULL, ccache); free(type); -diff --git a/configure.ac b/configure.ac -index e33462027..22f806787 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -5102,11 +5102,17 @@ AC_ARG_WITH([kerberos5], - # include +diff --color -ruNp a/configure.ac b/configure.ac +--- a/configure.ac 2026-03-12 11:51:16.580313749 +0100 ++++ b/configure.ac 2026-03-12 12:00:59.319587937 +0100 +@@ -4949,10 +4949,16 @@ AC_ARG_WITH([kerberos5], #elif defined(HAVE_GSSAPI_GSSAPI_GENERIC_H) # include -+#endif + #endif +#ifdef HAVE_GSSAPI_EXT_H +# include +#endif +#ifdef HAVE_GSSAPI_KRB5_H +# include - #endif ++#endif ]]) saved_LIBS="$LIBS" - LIBS="$LIBS $K5LIBS" @@ -46,11 +43,388 @@ index e33462027..22f806787 100644 LIBS="$saved_LIBS" fi -diff --git a/gss-serv-krb5.c b/gss-serv-krb5.c -index 2c786ef14..64e327917 100644 ---- a/gss-serv-krb5.c -+++ b/gss-serv-krb5.c -@@ -590,6 +590,231 @@ ssh_gssapi_krb5_updatecreds(ssh_gssapi_ccache *store, +diff --color -ruNp a/gss-serv.c b/gss-serv.c +--- a/gss-serv.c 2026-03-12 11:51:16.603196888 +0100 ++++ b/gss-serv.c 2026-03-12 12:01:44.171014784 +0100 +@@ -53,7 +53,7 @@ extern ServerOptions options; + + static ssh_gssapi_client gssapi_client = + { GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, GSS_C_NO_CREDENTIAL, +- GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL}; ++ GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL, 0}; + + ssh_gssapi_mech gssapi_null_mech = + { NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL, NULL}; +@@ -486,26 +486,351 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_g + return (ctx->major); + } + +-/* As user - called on fatal/exit */ ++/* Returns non-zero if Kerberos credentials have already been stored. */ ++int ++ssh_gssapi_credentials_stored(void) ++{ ++ return gssapi_client.store.envval != NULL; ++} ++ ++/* Returns a pointer to the credential-cache descriptor for this session. */ ++ssh_gssapi_ccache * ++ssh_gssapi_get_ccache(void) ++{ ++ return &gssapi_client.store; ++} ++ ++/* Log human-readable GSSAPI major and minor status strings. */ ++static void ++log_gss_error(OM_uint32 major, OM_uint32 minor, const char *label) ++{ ++ OM_uint32 lmin, mctx; ++ gss_buffer_desc emsg = GSS_C_EMPTY_BUFFER; ++ ++ mctx = 0; ++ do { ++ gss_display_status(&lmin, major, GSS_C_GSS_CODE, ++ GSS_C_NO_OID, &mctx, &emsg); ++ logit("%s: %.*s", label, (int)emsg.length, (char *)emsg.value); ++ gss_release_buffer(&lmin, &emsg); ++ } while (mctx != 0); ++ ++ mctx = 0; ++ do { ++ gss_display_status(&lmin, minor, GSS_C_MECH_CODE, ++ &gssapi_kerberos_mech.oid, &mctx, &emsg); ++ if (emsg.length > 0) ++ logit("%s: %.*s", label, ++ (int)emsg.length, (char *)emsg.value); ++ gss_release_buffer(&lmin, &emsg); ++ } while (mctx != 0); ++} ++ ++/* Log the canonical string form of a GSSAPI name as a debug message. */ ++static void ++debug_gss_name(const char *label, gss_name_t name) ++{ ++ OM_uint32 lmin; ++ gss_buffer_desc buf = GSS_C_EMPTY_BUFFER; ++ ++ if (gss_display_name(&lmin, name, &buf, NULL) == GSS_S_COMPLETE) { ++ debug_f("%s: %.*s", label, (int)buf.length, (char *)buf.value); ++ gss_release_buffer(&lmin, &buf); ++ } ++} ++ ++/* ++ * Check whether the user already has valid GSSAPI initiator credentials ++ * (e.g. a Kerberos TGT) in their default credential store with at least ++ * min_lifetime seconds remaining. Pass GSS_C_INDEFINITE to accept any ++ * positive remaining lifetime. Runs as the user. ++ * Returns 1 if sufficient credentials exist, 0 otherwise. ++ */ ++int ++ssh_gssapi_user_has_valid_tgt(u_int min_lifetime) ++{ ++ OM_uint32 major, minor, lifetime = 0; ++ gss_cred_id_t cred = GSS_C_NO_CREDENTIAL; ++ int found = 0; ++ ++ major = gss_acquire_cred(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, ++ GSS_C_NO_OID_SET, GSS_C_INITIATE, &cred, NULL, &lifetime); ++ if (!GSS_ERROR(major) && lifetime > 0 && ++ (min_lifetime == GSS_C_INDEFINITE || lifetime >= min_lifetime)) ++ found = 1; ++ if (cred != GSS_C_NO_CREDENTIAL) ++ gss_release_cred(&minor, &cred); ++ return found; ++} ++ ++ ++/* ++ * Perform S4U2Self (protocol transition): acquire a Kerberos service ticket ++ * for the SSH user on behalf of the host principal. Runs privileged. ++ * Populates gssapi_client.{creds,mech,displayname,exportedname} on success. ++ * Returns 0 on success, -1 on failure. ++ */ ++/* Privileged */ ++int ++ssh_gssapi_s4u2self(const char *user, u_int lifetime) ++{ ++ OM_uint32 major, minor, status; ++ gss_OID_set oidset = GSS_C_NO_OID_SET; ++ gss_name_t host_name = GSS_C_NO_NAME; ++ gss_name_t user_name = GSS_C_NO_NAME; ++ gss_cred_id_t host_creds = GSS_C_NO_CREDENTIAL; ++ gss_cred_id_t impersonated_creds = GSS_C_NO_CREDENTIAL; ++ gss_buffer_desc gssbuf, displayname = GSS_C_EMPTY_BUFFER; ++ char lname[NI_MAXHOST]; ++ char *val; ++ ++ if (gethostname(lname, sizeof(lname)) != 0) { ++ logit_f("gethostname: %s", strerror(errno)); ++ return -1; ++ } ++ ++ /* Acquire acceptor credential for host/ from the keytab */ ++ gss_create_empty_oid_set(&status, &oidset); ++ gss_add_oid_set_member(&status, &gssapi_kerberos_mech.oid, &oidset); ++ ++ xasprintf(&val, "host@%s", lname); ++ gssbuf.value = val; ++ gssbuf.length = strlen(val); ++ major = gss_import_name(&minor, &gssbuf, ++ GSS_C_NT_HOSTBASED_SERVICE, &host_name); ++ free(val); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name (host) failed"); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ debug_gss_name("host name parsed as", host_name); ++ ++ debug_f("acquiring host credentials as uid=%u euid=%u, principal=host@%s", ++ (unsigned)getuid(), (unsigned)geteuid(), lname); ++#ifdef HAVE_GSS_ACQUIRE_CRED_FROM ++ { ++ gss_key_value_element_desc store_elements[] = { ++ { "client_keytab", "/etc/krb5.keytab" }, ++ { "keytab", "/etc/krb5.keytab" }, ++ { "ccache", "MEMORY:" }, ++ }; ++ const gss_key_value_set_desc cred_store = { 3, store_elements }; ++ ++ major = gss_acquire_cred_from(&minor, host_name, lifetime, ++ oidset, GSS_C_BOTH, &cred_store, &host_creds, NULL, NULL); ++ } ++#else ++ major = gss_acquire_cred(&minor, host_name, lifetime, ++ oidset, GSS_C_BOTH, &host_creds, NULL, NULL); ++#endif ++ gss_release_name(&minor, &host_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_acquire_cred(host@%s) failed as uid=%u euid=%u", ++ lname, (unsigned)getuid(), (unsigned)geteuid()); ++ log_gss_error(major, minor, "S4U2Self: gss_acquire_cred"); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ ++ /* Import the SSH username as a GSSAPI/Kerberos name */ ++ gssbuf.value = (void *)user; ++ gssbuf.length = strlen(user); ++ major = gss_import_name(&minor, &gssbuf, ++ GSS_C_NT_USER_NAME, &user_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name (user) failed"); ++ gss_release_cred(&minor, &host_creds); ++ gss_release_oid_set(&status, &oidset); ++ return -1; ++ } ++ debug_gss_name("user name parsed as", user_name); ++ ++ /* S4U2Self: obtain a service ticket for the user without their creds */ ++ debug_f("calling gss_acquire_cred_impersonate_name for user %.100s", user); ++ major = gss_acquire_cred_impersonate_name(&minor, ++ host_creds, user_name, lifetime, ++ oidset, GSS_C_INITIATE, ++ &impersonated_creds, NULL, NULL); ++ ++ gss_release_cred(&minor, &host_creds); ++ gss_release_oid_set(&status, &oidset); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_acquire_cred_impersonate_name failed for %.100s", ++ user); ++ log_gss_error(major, minor, ++ "S4U2Self: gss_acquire_cred_impersonate_name"); ++ gss_release_name(&minor, &user_name); ++ return -1; ++ } ++ ++ /* Get the display name (Kerberos principal string) for storecreds */ ++ major = gss_display_name(&minor, user_name, &displayname, NULL); ++ gss_release_name(&minor, &user_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_display_name failed"); ++ gss_release_cred(&minor, &impersonated_creds); ++ return -1; ++ } ++ ++ /* Populate gssapi_client for storecreds_s4u2self and s4u2proxy */ ++ gssapi_client.mech = &gssapi_kerberos_mech; ++ gssapi_client.creds = impersonated_creds; ++ gssapi_client.displayname.value = xmalloc(displayname.length + 1); ++ memcpy(gssapi_client.displayname.value, ++ displayname.value, displayname.length); ++ ((char *)gssapi_client.displayname.value)[displayname.length] = '\0'; ++ gssapi_client.displayname.length = displayname.length; ++ /* ++ * exportedname is used by ssh_gssapi_krb5_storecreds → krb5_parse_name. ++ * gss_display_name for a user-name returns the canonical principal ++ * string (e.g. user@REALM) which krb5_parse_name can consume directly. ++ */ ++ gssapi_client.exportedname.value = xmalloc(displayname.length + 1); ++ memcpy(gssapi_client.exportedname.value, ++ displayname.value, displayname.length); ++ ((char *)gssapi_client.exportedname.value)[displayname.length] = '\0'; ++ gssapi_client.exportedname.length = displayname.length; ++ ++ gss_release_buffer(&minor, &displayname); ++ debug_f("S4U2Self succeeded for %.100s", user); ++ return 0; ++} ++ ++/* As user — write the S4U2Self ticket into a new ccache via mech->storecreds */ + void +-ssh_gssapi_cleanup_creds(void) ++ssh_gssapi_storecreds_s4u2self(void) + { +- krb5_ccache ccache = NULL; +- krb5_error_code problem; ++ if (gssapi_client.mech == NULL || gssapi_client.mech->storecreds == NULL) { ++ debug_f("no GSSAPI mechanism for storing S4U2Self credentials"); ++ return; ++ } ++ (*gssapi_client.mech->storecreds)(&gssapi_client); ++} ++ ++/* ++ * Perform S4U2Proxy for each configured service principal, then flush all ++ * resulting tickets into the user's ccache. Runs as user, after ++ * ssh_gssapi_storecreds_s4u2self() has created the ccache. ++ * ++ * gssapi_client.creds (the S4U2Self proxy credential) is passed as the ++ * initiator to gss_init_sec_context(); the GSSAPI library presents the TGT ++ * and evidence ticket to the KDC via S4U2Proxy TGS-REQ. The output token ++ * (AP-REQ) is discarded — we do not connect to the target service. ++ * ++ * After iterating all services, gss_store_cred() flushes the accumulated ++ * proxy service tickets from the credential's internal ccache into the ++ * KRB5CCNAME ccache that storecreds_s4u2self() already created. ++ */ ++/* As user */ ++void ++ssh_gssapi_s4u2proxy(char **services, u_int nservices, u_int lifetime) ++{ ++ OM_uint32 major, minor; ++ gss_buffer_desc service_buf, output_token = GSS_C_EMPTY_BUFFER; ++ gss_name_t target_name; ++ gss_ctx_id_t ctx; ++ u_int i; ++ ++ if (gssapi_client.creds == GSS_C_NO_CREDENTIAL) { ++ debug_f("no proxy credential available"); ++ return; ++ } ++ if (gssapi_client.store.envval == NULL) { ++ debug_f("no ccache path set; cannot store proxy tickets"); ++ return; ++ } + +- if (gssapi_client.store.data != NULL) { +- if ((problem = krb5_cc_resolve(gssapi_client.store.data, gssapi_client.store.envval, &ccache))) { +- debug_f("krb5_cc_resolve(): %.100s", +- krb5_get_err_text(gssapi_client.store.data, problem)); +- } else if ((problem = krb5_cc_destroy(gssapi_client.store.data, ccache))) { +- debug_f("krb5_cc_destroy(): %.100s", +- krb5_get_err_text(gssapi_client.store.data, problem)); +- } else { +- krb5_free_context(gssapi_client.store.data); +- gssapi_client.store.data = NULL; ++ debug_f("starting S4U2Proxy as uid=%u euid=%u, %u service(s), ccache=%s", ++ (unsigned)getuid(), (unsigned)geteuid(), nservices, ++ gssapi_client.store.envval); ++ ++ /* Point the GSSAPI library at the user's ccache for ticket storage */ ++ setenv("KRB5CCNAME", gssapi_client.store.envval, 1); ++ ++ for (i = 0; i < nservices; i++) { ++ ctx = GSS_C_NO_CONTEXT; ++ target_name = GSS_C_NO_NAME; ++ ++ service_buf.value = services[i]; ++ service_buf.length = strlen(services[i]); ++ ++ /* ++ * GSS_C_NO_OID: let the library determine the name type. ++ * With Kerberos as the active mechanism, a fully-qualified ++ * principal like "svc/host@REALM" is parsed correctly. ++ */ ++ major = gss_import_name(&minor, &service_buf, ++ GSS_C_NO_OID, &target_name); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_import_name failed for %.200s", ++ services[i]); ++ log_gss_error(major, minor, "S4U2Proxy: gss_import_name"); ++ continue; + } ++ debug_gss_name("target service name parsed as", target_name); ++ ++ debug_f("calling gss_init_sec_context for %.200s", services[i]); ++ major = gss_init_sec_context(&minor, ++ gssapi_client.creds, /* proxy credential */ ++ &ctx, target_name, ++ GSS_C_NO_OID, /* default mech (Kerberos) */ ++ 0, /* no flags, no mutual auth */ ++ lifetime, ++ GSS_C_NO_CHANNEL_BINDINGS, ++ GSS_C_NO_BUFFER, /* no input token */ ++ NULL, /* actual_mech_type */ ++ &output_token, ++ NULL, /* ret_flags */ ++ NULL); /* time_rec */ ++ ++ gss_release_buffer(&minor, &output_token); ++ gss_release_name(&minor, &target_name); ++ if (ctx != GSS_C_NO_CONTEXT) ++ gss_delete_sec_context(&minor, &ctx, GSS_C_NO_BUFFER); ++ ++ if (GSS_ERROR(major)) { ++ logit_f("S4U2Proxy for %.200s on behalf of %.200s failed", ++ services[i], ++ (char *)gssapi_client.displayname.value); ++ log_gss_error(major, minor, ++ "S4U2Proxy: gss_init_sec_context"); ++ } else ++ debug_f("S4U2Proxy ticket obtained for %.200s", ++ services[i]); + } ++ ++ /* ++ * Flush all proxy service tickets from the credential's internal ++ * ccache into the KRB5CCNAME ccache via gss_store_cred(). ++ */ ++ major = gss_store_cred(&minor, gssapi_client.creds, GSS_C_INITIATE, ++ GSS_C_NO_OID, 1 /* overwrite_cred */, 1 /* default_cred */, ++ NULL, NULL); ++ if (GSS_ERROR(major)) { ++ logit_f("gss_store_cred failed; proxy tickets may be missing"); ++ log_gss_error(major, minor, "S4U2Proxy: gss_store_cred"); ++ } ++ ++ unsetenv("KRB5CCNAME"); ++} ++ ++#ifndef KRB5 ++/* As user - called on fatal/exit; full implementation in gss-serv-krb5.c */ ++void ++ssh_gssapi_cleanup_creds(void) ++{ ++} ++ ++/* ++ * Filter the user's ccache; full implementation in gss-serv-krb5.c. ++ */ ++void ++ssh_gssapi_krb5_filter_ccache(u_int drop_flags, ++ char **proxy_services, u_int nproxy_services) ++{ + } ++#endif /* !KRB5 */ + + /* As user */ + int +diff --color -ruNp a/gss-serv-krb5.c b/gss-serv-krb5.c +--- a/gss-serv-krb5.c 2026-03-12 11:51:16.581364233 +0100 ++++ b/gss-serv-krb5.c 2026-03-12 12:01:20.402569731 +0100 +@@ -609,6 +609,231 @@ ssh_gssapi_krb5_updatecreds(ssh_gssapi_c return 1; } @@ -282,401 +656,20 @@ index 2c786ef14..64e327917 100644 ssh_gssapi_mech gssapi_kerberos_mech = { "toWM5Slw5Ew8Mqkay+al2g==", "Kerberos", -diff --git a/gss-serv.c b/gss-serv.c -index 165484db0..90e2d03ce 100644 ---- a/gss-serv.c -+++ b/gss-serv.c -@@ -54,7 +54,7 @@ extern ServerOptions options; - - static ssh_gssapi_client gssapi_client = - { GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, GSS_C_NO_CREDENTIAL, -- GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL}; -+ GSS_C_NO_NAME, NULL, {NULL, NULL, NULL, NULL, NULL}, 0, 0, NULL, 0}; - - ssh_gssapi_mech gssapi_null_mech = - { NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL, NULL}; -@@ -484,26 +484,351 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_gssapi_client *client) - return (ctx->major); - } - --/* As user - called on fatal/exit */ -+/* Returns non-zero if Kerberos credentials have already been stored. */ -+int -+ssh_gssapi_credentials_stored(void) -+{ -+ return gssapi_client.store.envval != NULL; -+} -+ -+/* Returns a pointer to the credential-cache descriptor for this session. */ -+ssh_gssapi_ccache * -+ssh_gssapi_get_ccache(void) -+{ -+ return &gssapi_client.store; -+} -+ -+/* Log human-readable GSSAPI major and minor status strings. */ -+static void -+log_gss_error(OM_uint32 major, OM_uint32 minor, const char *label) -+{ -+ OM_uint32 lmin, mctx; -+ gss_buffer_desc emsg = GSS_C_EMPTY_BUFFER; -+ -+ mctx = 0; -+ do { -+ gss_display_status(&lmin, major, GSS_C_GSS_CODE, -+ GSS_C_NO_OID, &mctx, &emsg); -+ logit("%s: %.*s", label, (int)emsg.length, (char *)emsg.value); -+ gss_release_buffer(&lmin, &emsg); -+ } while (mctx != 0); -+ -+ mctx = 0; -+ do { -+ gss_display_status(&lmin, minor, GSS_C_MECH_CODE, -+ &gssapi_kerberos_mech.oid, &mctx, &emsg); -+ if (emsg.length > 0) -+ logit("%s: %.*s", label, -+ (int)emsg.length, (char *)emsg.value); -+ gss_release_buffer(&lmin, &emsg); -+ } while (mctx != 0); -+} -+ -+/* Log the canonical string form of a GSSAPI name as a debug message. */ -+static void -+debug_gss_name(const char *label, gss_name_t name) -+{ -+ OM_uint32 lmin; -+ gss_buffer_desc buf = GSS_C_EMPTY_BUFFER; -+ -+ if (gss_display_name(&lmin, name, &buf, NULL) == GSS_S_COMPLETE) { -+ debug_f("%s: %.*s", label, (int)buf.length, (char *)buf.value); -+ gss_release_buffer(&lmin, &buf); -+ } -+} -+ -+/* -+ * Check whether the user already has valid GSSAPI initiator credentials -+ * (e.g. a Kerberos TGT) in their default credential store with at least -+ * min_lifetime seconds remaining. Pass GSS_C_INDEFINITE to accept any -+ * positive remaining lifetime. Runs as the user. -+ * Returns 1 if sufficient credentials exist, 0 otherwise. -+ */ -+int -+ssh_gssapi_user_has_valid_tgt(u_int min_lifetime) -+{ -+ OM_uint32 major, minor, lifetime = 0; -+ gss_cred_id_t cred = GSS_C_NO_CREDENTIAL; -+ int found = 0; -+ -+ major = gss_acquire_cred(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, -+ GSS_C_NO_OID_SET, GSS_C_INITIATE, &cred, NULL, &lifetime); -+ if (!GSS_ERROR(major) && lifetime > 0 && -+ (min_lifetime == GSS_C_INDEFINITE || lifetime >= min_lifetime)) -+ found = 1; -+ if (cred != GSS_C_NO_CREDENTIAL) -+ gss_release_cred(&minor, &cred); -+ return found; -+} -+ -+ -+/* -+ * Perform S4U2Self (protocol transition): acquire a Kerberos service ticket -+ * for the SSH user on behalf of the host principal. Runs privileged. -+ * Populates gssapi_client.{creds,mech,displayname,exportedname} on success. -+ * Returns 0 on success, -1 on failure. -+ */ -+/* Privileged */ -+int -+ssh_gssapi_s4u2self(const char *user, u_int lifetime) -+{ -+ OM_uint32 major, minor, status; -+ gss_OID_set oidset = GSS_C_NO_OID_SET; -+ gss_name_t host_name = GSS_C_NO_NAME; -+ gss_name_t user_name = GSS_C_NO_NAME; -+ gss_cred_id_t host_creds = GSS_C_NO_CREDENTIAL; -+ gss_cred_id_t impersonated_creds = GSS_C_NO_CREDENTIAL; -+ gss_buffer_desc gssbuf, displayname = GSS_C_EMPTY_BUFFER; -+ char lname[NI_MAXHOST]; -+ char *val; -+ -+ if (gethostname(lname, sizeof(lname)) != 0) { -+ logit_f("gethostname: %s", strerror(errno)); -+ return -1; -+ } -+ -+ /* Acquire acceptor credential for host/ from the keytab */ -+ gss_create_empty_oid_set(&status, &oidset); -+ gss_add_oid_set_member(&status, &gssapi_kerberos_mech.oid, &oidset); -+ -+ xasprintf(&val, "host@%s", lname); -+ gssbuf.value = val; -+ gssbuf.length = strlen(val); -+ major = gss_import_name(&minor, &gssbuf, -+ GSS_C_NT_HOSTBASED_SERVICE, &host_name); -+ free(val); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_import_name (host) failed"); -+ gss_release_oid_set(&status, &oidset); -+ return -1; -+ } -+ debug_gss_name("host name parsed as", host_name); -+ -+ debug_f("acquiring host credentials as uid=%u euid=%u, principal=host@%s", -+ (unsigned)getuid(), (unsigned)geteuid(), lname); -+#ifdef HAVE_GSS_ACQUIRE_CRED_FROM -+ { -+ gss_key_value_element_desc store_elements[] = { -+ { "client_keytab", "/etc/krb5.keytab" }, -+ { "keytab", "/etc/krb5.keytab" }, -+ { "ccache", "MEMORY:" }, -+ }; -+ const gss_key_value_set_desc cred_store = { 3, store_elements }; -+ -+ major = gss_acquire_cred_from(&minor, host_name, lifetime, -+ oidset, GSS_C_BOTH, &cred_store, &host_creds, NULL, NULL); -+ } -+#else -+ major = gss_acquire_cred(&minor, host_name, lifetime, -+ oidset, GSS_C_BOTH, &host_creds, NULL, NULL); -+#endif -+ gss_release_name(&minor, &host_name); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_acquire_cred(host@%s) failed as uid=%u euid=%u", -+ lname, (unsigned)getuid(), (unsigned)geteuid()); -+ log_gss_error(major, minor, "S4U2Self: gss_acquire_cred"); -+ gss_release_oid_set(&status, &oidset); -+ return -1; -+ } -+ -+ /* Import the SSH username as a GSSAPI/Kerberos name */ -+ gssbuf.value = (void *)user; -+ gssbuf.length = strlen(user); -+ major = gss_import_name(&minor, &gssbuf, -+ GSS_C_NT_USER_NAME, &user_name); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_import_name (user) failed"); -+ gss_release_cred(&minor, &host_creds); -+ gss_release_oid_set(&status, &oidset); -+ return -1; -+ } -+ debug_gss_name("user name parsed as", user_name); -+ -+ /* S4U2Self: obtain a service ticket for the user without their creds */ -+ debug_f("calling gss_acquire_cred_impersonate_name for user %.100s", user); -+ major = gss_acquire_cred_impersonate_name(&minor, -+ host_creds, user_name, lifetime, -+ oidset, GSS_C_INITIATE, -+ &impersonated_creds, NULL, NULL); -+ -+ gss_release_cred(&minor, &host_creds); -+ gss_release_oid_set(&status, &oidset); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_acquire_cred_impersonate_name failed for %.100s", -+ user); -+ log_gss_error(major, minor, -+ "S4U2Self: gss_acquire_cred_impersonate_name"); -+ gss_release_name(&minor, &user_name); -+ return -1; -+ } -+ -+ /* Get the display name (Kerberos principal string) for storecreds */ -+ major = gss_display_name(&minor, user_name, &displayname, NULL); -+ gss_release_name(&minor, &user_name); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_display_name failed"); -+ gss_release_cred(&minor, &impersonated_creds); -+ return -1; -+ } -+ -+ /* Populate gssapi_client for storecreds_s4u2self and s4u2proxy */ -+ gssapi_client.mech = &gssapi_kerberos_mech; -+ gssapi_client.creds = impersonated_creds; -+ gssapi_client.displayname.value = xmalloc(displayname.length + 1); -+ memcpy(gssapi_client.displayname.value, -+ displayname.value, displayname.length); -+ ((char *)gssapi_client.displayname.value)[displayname.length] = '\0'; -+ gssapi_client.displayname.length = displayname.length; -+ /* -+ * exportedname is used by ssh_gssapi_krb5_storecreds → krb5_parse_name. -+ * gss_display_name for a user-name returns the canonical principal -+ * string (e.g. user@REALM) which krb5_parse_name can consume directly. -+ */ -+ gssapi_client.exportedname.value = xmalloc(displayname.length + 1); -+ memcpy(gssapi_client.exportedname.value, -+ displayname.value, displayname.length); -+ ((char *)gssapi_client.exportedname.value)[displayname.length] = '\0'; -+ gssapi_client.exportedname.length = displayname.length; -+ -+ gss_release_buffer(&minor, &displayname); -+ debug_f("S4U2Self succeeded for %.100s", user); -+ return 0; -+} -+ -+/* As user — write the S4U2Self ticket into a new ccache via mech->storecreds */ - void --ssh_gssapi_cleanup_creds(void) -+ssh_gssapi_storecreds_s4u2self(void) - { -- krb5_ccache ccache = NULL; -- krb5_error_code problem; -- -- if (gssapi_client.store.data != NULL) { -- if ((problem = krb5_cc_resolve(gssapi_client.store.data, gssapi_client.store.envval, &ccache))) { -- debug_f("krb5_cc_resolve(): %.100s", -- krb5_get_err_text(gssapi_client.store.data, problem)); -- } else if ((problem = krb5_cc_destroy(gssapi_client.store.data, ccache))) { -- debug_f("krb5_cc_destroy(): %.100s", -- krb5_get_err_text(gssapi_client.store.data, problem)); -- } else { -- krb5_free_context(gssapi_client.store.data); -- gssapi_client.store.data = NULL; -+ if (gssapi_client.mech == NULL || gssapi_client.mech->storecreds == NULL) { -+ debug_f("no GSSAPI mechanism for storing S4U2Self credentials"); -+ return; -+ } -+ (*gssapi_client.mech->storecreds)(&gssapi_client); -+} -+ -+/* -+ * Perform S4U2Proxy for each configured service principal, then flush all -+ * resulting tickets into the user's ccache. Runs as user, after -+ * ssh_gssapi_storecreds_s4u2self() has created the ccache. -+ * -+ * gssapi_client.creds (the S4U2Self proxy credential) is passed as the -+ * initiator to gss_init_sec_context(); the GSSAPI library presents the TGT -+ * and evidence ticket to the KDC via S4U2Proxy TGS-REQ. The output token -+ * (AP-REQ) is discarded — we do not connect to the target service. -+ * -+ * After iterating all services, gss_store_cred() flushes the accumulated -+ * proxy service tickets from the credential's internal ccache into the -+ * KRB5CCNAME ccache that storecreds_s4u2self() already created. -+ */ -+/* As user */ -+void -+ssh_gssapi_s4u2proxy(char **services, u_int nservices, u_int lifetime) -+{ -+ OM_uint32 major, minor; -+ gss_buffer_desc service_buf, output_token = GSS_C_EMPTY_BUFFER; -+ gss_name_t target_name; -+ gss_ctx_id_t ctx; -+ u_int i; -+ -+ if (gssapi_client.creds == GSS_C_NO_CREDENTIAL) { -+ debug_f("no proxy credential available"); -+ return; -+ } -+ if (gssapi_client.store.envval == NULL) { -+ debug_f("no ccache path set; cannot store proxy tickets"); -+ return; -+ } -+ -+ debug_f("starting S4U2Proxy as uid=%u euid=%u, %u service(s), ccache=%s", -+ (unsigned)getuid(), (unsigned)geteuid(), nservices, -+ gssapi_client.store.envval); -+ -+ /* Point the GSSAPI library at the user's ccache for ticket storage */ -+ setenv("KRB5CCNAME", gssapi_client.store.envval, 1); -+ -+ for (i = 0; i < nservices; i++) { -+ ctx = GSS_C_NO_CONTEXT; -+ target_name = GSS_C_NO_NAME; -+ -+ service_buf.value = services[i]; -+ service_buf.length = strlen(services[i]); -+ -+ /* -+ * GSS_C_NO_OID: let the library determine the name type. -+ * With Kerberos as the active mechanism, a fully-qualified -+ * principal like "svc/host@REALM" is parsed correctly. -+ */ -+ major = gss_import_name(&minor, &service_buf, -+ GSS_C_NO_OID, &target_name); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_import_name failed for %.200s", -+ services[i]); -+ log_gss_error(major, minor, "S4U2Proxy: gss_import_name"); -+ continue; - } -+ debug_gss_name("target service name parsed as", target_name); -+ -+ debug_f("calling gss_init_sec_context for %.200s", services[i]); -+ major = gss_init_sec_context(&minor, -+ gssapi_client.creds, /* proxy credential */ -+ &ctx, target_name, -+ GSS_C_NO_OID, /* default mech (Kerberos) */ -+ 0, /* no flags, no mutual auth */ -+ lifetime, -+ GSS_C_NO_CHANNEL_BINDINGS, -+ GSS_C_NO_BUFFER, /* no input token */ -+ NULL, /* actual_mech_type */ -+ &output_token, -+ NULL, /* ret_flags */ -+ NULL); /* time_rec */ -+ -+ gss_release_buffer(&minor, &output_token); -+ gss_release_name(&minor, &target_name); -+ if (ctx != GSS_C_NO_CONTEXT) -+ gss_delete_sec_context(&minor, &ctx, GSS_C_NO_BUFFER); -+ -+ if (GSS_ERROR(major)) { -+ logit_f("S4U2Proxy for %.200s on behalf of %.200s failed", -+ services[i], -+ (char *)gssapi_client.displayname.value); -+ log_gss_error(major, minor, -+ "S4U2Proxy: gss_init_sec_context"); -+ } else -+ debug_f("S4U2Proxy ticket obtained for %.200s", -+ services[i]); - } -+ -+ /* -+ * Flush all proxy service tickets from the credential's internal -+ * ccache into the KRB5CCNAME ccache via gss_store_cred(). -+ */ -+ major = gss_store_cred(&minor, gssapi_client.creds, GSS_C_INITIATE, -+ GSS_C_NO_OID, 1 /* overwrite_cred */, 1 /* default_cred */, -+ NULL, NULL); -+ if (GSS_ERROR(major)) { -+ logit_f("gss_store_cred failed; proxy tickets may be missing"); -+ log_gss_error(major, minor, "S4U2Proxy: gss_store_cred"); -+ } -+ -+ unsetenv("KRB5CCNAME"); -+} -+ -+#ifndef KRB5 -+/* As user - called on fatal/exit; full implementation in gss-serv-krb5.c */ -+void -+ssh_gssapi_cleanup_creds(void) -+{ -+} -+ -+/* -+ * Filter the user's ccache; full implementation in gss-serv-krb5.c. -+ */ -+void -+ssh_gssapi_krb5_filter_ccache(u_int drop_flags, -+ char **proxy_services, u_int nproxy_services) -+{ - } -+#endif /* !KRB5 */ - - /* As user */ - int -diff --git a/servconf.c b/servconf.c -index 1093dcbac..019070505 100644 ---- a/servconf.c -+++ b/servconf.c -@@ -149,6 +149,9 @@ initialize_server_options(ServerOptions *options) +diff --color -ruNp a/servconf.c b/servconf.c +--- a/servconf.c 2026-03-12 11:51:16.603765849 +0100 ++++ b/servconf.c 2026-03-12 12:07:51.450992249 +0100 +@@ -148,6 +148,9 @@ initialize_server_options(ServerOptions + options->gss_indicators = NULL; options->gss_store_rekey = -1; options->gss_kex_algorithms = NULL; - options->gss_indicators = NULL; + options->gss_allow_s4u2self = -1; + options->gss_proxy_services = NULL; + options->num_gss_proxy_services = 0; options->use_kuserok = -1; options->enable_k5users = -1; options->password_authentication = -1; -@@ -404,6 +407,8 @@ fill_default_server_options(ServerOptions *options) +@@ -407,6 +410,8 @@ fill_default_server_options(ServerOption options->gss_deleg_creds = 1; if (options->gss_strict_acceptor == -1) options->gss_strict_acceptor = 1; @@ -685,7 +678,7 @@ index 1093dcbac..019070505 100644 if (options->gss_store_rekey == -1) options->gss_store_rekey = 0; #ifdef GSSAPI -@@ -599,7 +604,7 @@ typedef enum { +@@ -603,7 +608,7 @@ typedef enum { sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, sGssAuthentication, sGssCleanupCreds, sGssDelegateCreds, sGssEnablek5users, sGssStrictAcceptor, @@ -694,7 +687,7 @@ index 1093dcbac..019070505 100644 sAcceptEnv, sSetEnv, sPermitTunnel, sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, sUsePrivilegeSeparation, sAllowAgentForwarding, -@@ -697,6 +702,8 @@ static struct { +@@ -701,6 +706,8 @@ static struct { { "gssapikexalgorithms", sGssKexAlgorithms, SSHCFG_GLOBAL }, { "gssapienablek5users", sGssEnablek5users, SSHCFG_ALL }, { "gssapiindicators", sGssIndicators, SSHCFG_ALL }, @@ -703,7 +696,7 @@ index 1093dcbac..019070505 100644 #else { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, -@@ -708,6 +715,8 @@ static struct { +@@ -712,6 +719,8 @@ static struct { { "gssapikexalgorithms", sUnsupported, SSHCFG_GLOBAL }, { "gssapienablek5users", sUnsupported, SSHCFG_ALL }, { "gssapiindicators", sUnsupported, SSHCFG_ALL }, @@ -712,7 +705,7 @@ index 1093dcbac..019070505 100644 #endif { "gssusesessionccache", sUnsupported, SSHCFG_GLOBAL }, { "gssapiusesessioncredcache", sUnsupported, SSHCFG_GLOBAL }, -@@ -1748,6 +1757,44 @@ process_server_config_line_depth(ServerOptions *options, char *line, +@@ -1742,6 +1751,44 @@ process_server_config_line_depth(ServerO options->gss_indicators = xstrdup(arg); break; @@ -757,7 +750,7 @@ index 1093dcbac..019070505 100644 case sPasswordAuthentication: intptr = &options->password_authentication; goto parse_flag; -@@ -3026,6 +3073,7 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth) +@@ -3010,6 +3057,7 @@ copy_set_server_options(ServerOptions *d M_CP_INTOPT(password_authentication); M_CP_INTOPT(gss_authentication); @@ -765,7 +758,7 @@ index 1093dcbac..019070505 100644 M_CP_INTOPT(pubkey_authentication); M_CP_INTOPT(pubkey_auth_options); M_CP_INTOPT(kerberos_authentication); -@@ -3379,6 +3427,15 @@ dump_config(ServerOptions *o) +@@ -3364,6 +3412,15 @@ dump_config(ServerOptions *o) dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey); dump_cfg_string(sGssKexAlgorithms, o->gss_kex_algorithms); dump_cfg_string(sGssIndicators, o->gss_indicators); @@ -781,10 +774,9 @@ index 1093dcbac..019070505 100644 #endif dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication); dump_cfg_fmtint(sKbdInteractiveAuthentication, -diff --git a/servconf.h b/servconf.h -index 6bfdf6305..40dcd29d8 100644 ---- a/servconf.h -+++ b/servconf.h +diff --color -ruNp a/servconf.h b/servconf.h +--- a/servconf.h 2026-03-12 11:51:16.604292877 +0100 ++++ b/servconf.h 2026-03-12 12:02:44.689745248 +0100 @@ -160,6 +160,9 @@ typedef struct { int gss_cleanup_creds; /* If true, destroy cred cache on logout */ int gss_deleg_creds; /* If true, accept delegated GSS credentials */ @@ -795,7 +787,7 @@ index 6bfdf6305..40dcd29d8 100644 int gss_store_rekey; char *gss_kex_algorithms; /* GSSAPI kex methods to be offered by client. */ int password_authentication; /* If true, permit password -@@ -313,6 +316,7 @@ TAILQ_HEAD(include_list, include_item); +@@ -314,6 +317,7 @@ TAILQ_HEAD(include_list, include_item); M_CP_STROPT(permit_user_env_allowlist); \ M_CP_STROPT(pam_service_name); \ M_CP_STROPT(gss_indicators); \ @@ -803,42 +795,90 @@ index 6bfdf6305..40dcd29d8 100644 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 1506719a9..a0d51c9be 100644 ---- a/ssh-gss.h -+++ b/ssh-gss.h -@@ -119,6 +119,7 @@ typedef struct { - int used; - int updated; - char **indicators; /* auth indicators */ -+ int allow_self; /* allow protocol transition */ - } ssh_gssapi_client; - - typedef struct ssh_gssapi_mech_struct { -@@ -199,6 +200,18 @@ OM_uint32 ssh_gssapi_checkmic(Gssctxt *, gss_buffer_t, gss_buffer_t); - void ssh_gssapi_do_child(char ***, u_int *); - void ssh_gssapi_cleanup_creds(void); - int ssh_gssapi_storecreds(void); -+int ssh_gssapi_credentials_stored(void); -+ssh_gssapi_ccache *ssh_gssapi_get_ccache(void); -+int ssh_gssapi_user_has_valid_tgt(u_int); -+int ssh_gssapi_user_has_valid_proxy_tickets(char **, u_int, u_int); -+int ssh_gssapi_s4u2self(const char *, u_int); -+void ssh_gssapi_storecreds_s4u2self(void); -+void ssh_gssapi_s4u2proxy(char **, u_int, u_int); -+/* Flags for ssh_gssapi_krb5_filter_ccache(): which ticket classes to remove */ -+#define SSH_GSSAPI_CCFILTER_TGT (1u << 0) /* krbtgt/... entries */ -+#define SSH_GSSAPI_CCFILTER_SELF (1u << 1) /* S4U2Self evidence ticket */ -+#define SSH_GSSAPI_CCFILTER_PROXY (1u << 2) /* S4U2Proxy service tickets */ -+void ssh_gssapi_krb5_filter_ccache(u_int, char **, u_int); - const char *ssh_gssapi_displayname(void); - - char *ssh_gssapi_server_mechanisms(void); -diff --git a/sshd-session.c b/sshd-session.c -index a558bbc33..d8b6af13f 100644 ---- a/sshd-session.c -+++ b/sshd-session.c -@@ -1425,6 +1425,104 @@ main(int ac, char **av) +diff --color -ruNp a/sshd_config.5 b/sshd_config.5 +--- a/sshd_config.5 2026-03-12 11:51:16.604904588 +0100 ++++ b/sshd_config.5 2026-03-12 12:10:03.542653814 +0100 +@@ -834,6 +834,76 @@ FIDO2-based pre-authentication in FreeIP + The default + .Dq none + is to not use GSSAPI authentication indicators for access decisions. ++.It Cm GSSAPIAllowS4U2Self ++Controls whether the SSH server performs a Kerberos protocol transition ++(S4U2Self) after a successful authentication using any other method. ++Accepted values are ++.Cm no , ++.Cm yes , ++or a time interval (see ++.Sx TIME FORMATS ++below). ++.Cm no ++disables S4U2Self entirely. ++.Cm yes ++enables S4U2Self and requests a ticket with the maximum lifetime ++permitted by the KDC. ++A time interval (e.g.\& ++.Cm 8h , ++.Cm 1d ) ++enables S4U2Self and requests a ticket valid for at most that duration. ++The option is a no-op when delegated GSSAPI credentials are already available. ++The obtained service ticket is stored in the default credentials cache and is ++accessible to any application that has access to the Kerberos host principal ++.Pq host/machine.fqdn@REALM ++credentials on the same host. ++.Pp ++The default is ++.Cm no . ++.It Cm GSSAPIS42UProxyServices ++Specifies a list of Kerberos service principals for which constrained ++delegation (S4U2Proxy) tickets should be obtained after a successful ++S4U2Self protocol transition. ++Each entry must be a fully-qualified Kerberos principal name of the form ++.Ar service/host@REALM . ++Multiple principals may be listed, separated by whitespace. ++The keyword ++.Cm none ++clears any previously set list. ++.Pp ++This option may be used independently of ++.Cm GSSAPIAllowS4U2Self . ++When S4U2Self succeeds, the server iterates the list and calls ++.Xr gss_init_sec_context 3 ++for each principal with the proxy credential obtained by S4U2Self as ++the initiator. ++The GSSAPI library presents the evidence ticket to the KDC via an ++S4U2Proxy TGS-REQ; if the host service holds the necessary constrained- ++delegation permission in the KDC, a service ticket from the user to ++the target service is issued. ++These tickets are accumulated and then flushed into the user's ccache ++via ++.Xr gss_store_cred 3 , ++so that any application running in the user's session can use them ++without further interaction. ++The AP-REQ output token of each ++.Xr gss_init_sec_context 3 ++call is discarded; no network connection to the target service is made. ++.Pp ++When used together with ++.Cm GSSAPIAllowS4U2Self , ++the TGT and S4U2Self ticket are also stored in the user's ccache in ++addition to the S4U2Proxy service tickets. ++When used alone (without ++.Cm GSSAPIAllowS4U2Self ) , ++only the S4U2Proxy service tickets are stored; the intermediate S4U2Self ++credential is not placed in the user's ccache. ++.Pp ++This option supports ++.Cm Match ++blocks, allowing per-user or per-host lists of delegation targets. ++.Pp ++The default is empty (no S4U2Proxy delegation is performed). + .It Cm HostbasedAcceptedAlgorithms + The default is handled system-wide by + .Xr crypto-policies 7 . +diff --color -ruNp a/sshd-session.c b/sshd-session.c +--- a/sshd-session.c 2026-03-12 11:51:16.597852696 +0100 ++++ b/sshd-session.c 2026-03-12 12:03:24.375297257 +0100 +@@ -1489,6 +1489,104 @@ main(int ac, char **av) authctxt->krb5_set_env = ssh_gssapi_storecreds(); restore_uid(); } @@ -943,84 +983,33 @@ index a558bbc33..d8b6af13f 100644 #endif #ifdef WITH_SELINUX sshd_selinux_setup_exec_context(authctxt->pw->pw_name, -diff --git a/sshd_config.5 b/sshd_config.5 -index 3dbce55fc..d25b406fb 100644 ---- a/sshd_config.5 -+++ b/sshd_config.5 -@@ -832,6 +832,76 @@ 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 GSSAPIAllowS4U2Self -+Controls whether the SSH server performs a Kerberos protocol transition -+(S4U2Self) after a successful authentication using any other method. -+Accepted values are -+.Cm no , -+.Cm yes , -+or a time interval (see -+.Sx TIME FORMATS -+below). -+.Cm no -+disables S4U2Self entirely. -+.Cm yes -+enables S4U2Self and requests a ticket with the maximum lifetime -+permitted by the KDC. -+A time interval (e.g.\& -+.Cm 8h , -+.Cm 1d ) -+enables S4U2Self and requests a ticket valid for at most that duration. -+The option is a no-op when delegated GSSAPI credentials are already available. -+The obtained service ticket is stored in the default credentials cache and is -+accessible to any application that has access to the Kerberos host principal -+.Pq host/machine.fqdn@REALM -+credentials on the same host. -+.Pp -+The default is -+.Cm no . -+.It Cm GSSAPIS42UProxyServices -+Specifies a list of Kerberos service principals for which constrained -+delegation (S4U2Proxy) tickets should be obtained after a successful -+S4U2Self protocol transition. -+Each entry must be a fully-qualified Kerberos principal name of the form -+.Ar service/host@REALM . -+Multiple principals may be listed, separated by whitespace. -+The keyword -+.Cm none -+clears any previously set list. -+.Pp -+This option may be used independently of -+.Cm GSSAPIAllowS4U2Self . -+When S4U2Self succeeds, the server iterates the list and calls -+.Xr gss_init_sec_context 3 -+for each principal with the proxy credential obtained by S4U2Self as -+the initiator. -+The GSSAPI library presents the evidence ticket to the KDC via an -+S4U2Proxy TGS-REQ; if the host service holds the necessary constrained- -+delegation permission in the KDC, a service ticket from the user to -+the target service is issued. -+These tickets are accumulated and then flushed into the user's ccache -+via -+.Xr gss_store_cred 3 , -+so that any application running in the user's session can use them -+without further interaction. -+The AP-REQ output token of each -+.Xr gss_init_sec_context 3 -+call is discarded; no network connection to the target service is made. -+.Pp -+When used together with -+.Cm GSSAPIAllowS4U2Self , -+the TGT and S4U2Self ticket are also stored in the user's ccache in -+addition to the S4U2Proxy service tickets. -+When used alone (without -+.Cm GSSAPIAllowS4U2Self ) , -+only the S4U2Proxy service tickets are stored; the intermediate S4U2Self -+credential is not placed in the user's ccache. -+.Pp -+This option supports -+.Cm Match -+blocks, allowing per-user or per-host lists of delegation targets. -+.Pp -+The default is empty (no S4U2Proxy delegation is performed). - .It Cm HostbasedAcceptedAlgorithms - The default is handled system-wide by - .Xr crypto-policies 7 . +diff --color -ruNp a/ssh-gss.h b/ssh-gss.h +--- a/ssh-gss.h 2026-03-12 11:51:16.583291666 +0100 ++++ b/ssh-gss.h 2026-03-12 12:03:11.541329687 +0100 +@@ -119,6 +119,7 @@ typedef struct { + int used; + int updated; + char **indicators; /* auth indicators */ ++ int allow_self; /* allow protocol transition */ + } ssh_gssapi_client; + + typedef struct ssh_gssapi_mech_struct { +@@ -199,6 +200,18 @@ OM_uint32 ssh_gssapi_checkmic(Gssctxt *, + void ssh_gssapi_do_child(char ***, u_int *); + void ssh_gssapi_cleanup_creds(void); + int ssh_gssapi_storecreds(void); ++int ssh_gssapi_credentials_stored(void); ++ssh_gssapi_ccache *ssh_gssapi_get_ccache(void); ++int ssh_gssapi_user_has_valid_tgt(u_int); ++int ssh_gssapi_user_has_valid_proxy_tickets(char **, u_int, u_int); ++int ssh_gssapi_s4u2self(const char *, u_int); ++void ssh_gssapi_storecreds_s4u2self(void); ++void ssh_gssapi_s4u2proxy(char **, u_int, u_int); ++/* Flags for ssh_gssapi_krb5_filter_ccache(): which ticket classes to remove */ ++#define SSH_GSSAPI_CCFILTER_TGT (1u << 0) /* krbtgt/... entries */ ++#define SSH_GSSAPI_CCFILTER_SELF (1u << 1) /* S4U2Self evidence ticket */ ++#define SSH_GSSAPI_CCFILTER_PROXY (1u << 2) /* S4U2Proxy service tickets */ ++void ssh_gssapi_krb5_filter_ccache(u_int, char **, u_int); + const char *ssh_gssapi_displayname(void); + + char *ssh_gssapi_server_mechanisms(void); diff --git a/openssh-9.9p1-sshd-no-delegate-credentials.patch b/openssh-9.9p1-sshd-no-delegate-credentials.patch index 2b70103..59d0120 100644 --- a/openssh-9.9p1-sshd-no-delegate-credentials.patch +++ b/openssh-9.9p1-sshd-no-delegate-credentials.patch @@ -1,8 +1,7 @@ -diff --git a/gss-serv.c b/gss-serv.c -index 5c0491cf1..e2c501d0c 100644 ---- a/gss-serv.c -+++ b/gss-serv.c -@@ -509,6 +509,11 @@ ssh_gssapi_cleanup_creds(void) +diff --color -ruNp a/gss-serv.c b/gss-serv.c +--- a/gss-serv.c 2026-03-11 13:54:53.076924823 +0100 ++++ b/gss-serv.c 2026-03-11 14:10:41.232855086 +0100 +@@ -493,6 +493,11 @@ ssh_gssapi_cleanup_creds(void) int ssh_gssapi_storecreds(void) { @@ -14,19 +13,18 @@ index 5c0491cf1..e2c501d0c 100644 if (gssapi_client.mech && gssapi_client.mech->storecreds) { return (*gssapi_client.mech->storecreds)(&gssapi_client); } else -diff --git a/servconf.c b/servconf.c -index aab653244..02a9888c9 100644 ---- a/servconf.c -+++ b/servconf.c -@@ -144,6 +144,7 @@ initialize_server_options(ServerOptions *options) +diff --color -ruNp a/servconf.c b/servconf.c +--- a/servconf.c 2026-03-11 13:54:53.086263187 +0100 ++++ b/servconf.c 2026-03-11 14:03:10.708713524 +0100 +@@ -143,6 +143,7 @@ initialize_server_options(ServerOptions options->gss_authentication=-1; options->gss_keyex = -1; options->gss_cleanup_creds = -1; + options->gss_deleg_creds = -1; options->gss_strict_acceptor = -1; + options->gss_indicators = NULL; options->gss_store_rekey = -1; - options->gss_kex_algorithms = NULL; -@@ -403,6 +404,8 @@ fill_default_server_options(ServerOptions *options) +@@ -402,6 +403,8 @@ fill_default_server_options(ServerOption options->gss_keyex = 0; if (options->gss_cleanup_creds == -1) options->gss_cleanup_creds = 1; @@ -61,7 +59,7 @@ index aab653244..02a9888c9 100644 { "gssapistrictacceptorcheck", sUnsupported, SSHCFG_GLOBAL }, { "gssapikeyexchange", sUnsupported, SSHCFG_GLOBAL }, { "gssapistorecredentialsonrekey", sUnsupported, SSHCFG_GLOBAL }, -@@ -1713,6 +1719,10 @@ process_server_config_line_depth(ServerOptions *options, char *line, +@@ -1703,6 +1709,10 @@ process_server_config_line_depth(ServerO intptr = &options->gss_cleanup_creds; goto parse_flag; @@ -72,7 +70,7 @@ index aab653244..02a9888c9 100644 case sGssStrictAcceptor: intptr = &options->gss_strict_acceptor; goto parse_flag; -@@ -3359,6 +3369,7 @@ dump_config(ServerOptions *o) +@@ -3348,6 +3358,7 @@ dump_config(ServerOptions *o) #ifdef GSSAPI dump_cfg_fmtint(sGssAuthentication, o->gss_authentication); dump_cfg_fmtint(sGssCleanupCreds, o->gss_cleanup_creds); @@ -80,10 +78,9 @@ index aab653244..02a9888c9 100644 dump_cfg_fmtint(sGssKeyEx, o->gss_keyex); dump_cfg_fmtint(sGssStrictAcceptor, o->gss_strict_acceptor); dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey); -diff --git a/servconf.h b/servconf.h -index 7c41df417..6bfdf6305 100644 ---- a/servconf.h -+++ b/servconf.h +diff --color -ruNp a/servconf.h b/servconf.h +--- a/servconf.h 2026-03-11 13:54:53.086763709 +0100 ++++ b/servconf.h 2026-03-11 14:13:51.130708769 +0100 @@ -158,6 +158,7 @@ typedef struct { int gss_authentication; /* If true, permit GSSAPI authentication */ int gss_keyex; /* If true, permit GSSAPI key exchange */ @@ -92,11 +89,10 @@ index 7c41df417..6bfdf6305 100644 int gss_strict_acceptor; /* If true, restrict the GSSAPI acceptor name */ int gss_store_rekey; char *gss_kex_algorithms; /* GSSAPI kex methods to be offered by client. */ -diff --git a/sshd_config.0 b/sshd_config.0 -index 49349bb30..e798f4df5 100644 ---- a/sshd_config.0 -+++ b/sshd_config.0 -@@ -453,6 +453,9 @@ DESCRIPTION +diff --color -ruNp a/sshd_config.0 b/sshd_config.0 +--- a/sshd_config.0 2026-03-11 13:54:52.904233471 +0100 ++++ b/sshd_config.0 2026-03-11 14:12:35.341170737 +0100 +@@ -451,6 +451,9 @@ DESCRIPTION Specifies whether to automatically destroy the user's credentials cache on logout. The default is yes. @@ -106,11 +102,10 @@ index 49349bb30..e798f4df5 100644 GSSAPIStrictAcceptorCheck Determines whether to be strict about the identity of the GSSAPI acceptor a client authenticates against. If set to yes then the -diff --git a/sshd_config.5 b/sshd_config.5 -index 90ab87edd..8c677bfd0 100644 ---- a/sshd_config.5 -+++ b/sshd_config.5 -@@ -733,6 +733,9 @@ Specifies whether to automatically destroy the user's credentials cache +diff --color -ruNp a/sshd_config.5 b/sshd_config.5 +--- a/sshd_config.5 2026-03-11 13:54:53.087046352 +0100 ++++ b/sshd_config.5 2026-03-11 14:05:15.657248031 +0100 +@@ -733,6 +733,9 @@ Specifies whether to automatically destr on logout. The default is .Cm yes . diff --git a/openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch b/openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch index 237e45d..6c55a66 100644 --- a/openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch +++ b/openssh-9.9p1-support-authentication-indicators-in-GSSAPI.patch @@ -1,38 +1,7 @@ -From 5d5a66e96ad03132f65371070f4fa475f10207d9 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 | 64 +++++++++++++++++++++++++++--- - gss-serv.c | 103 +++++++++++++++++++++++++++++++++++++++++++++++- - servconf.c | 15 ++++++- - servconf.h | 2 + - ssh-gss.h | 7 ++++ - sshd_config.5 | 44 +++++++++++++++++++++ - 7 files changed, 228 insertions(+), 8 deletions(-) - -diff --git a/configure.ac b/configure.ac -index d92a85809..2cbe20bf3 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -5004,6 +5004,7 @@ AC_ARG_WITH([kerberos5], +diff --color -ruNp a/configure.ac b/configure.ac +--- a/configure.ac 2026-03-10 12:43:36.860784813 +0100 ++++ b/configure.ac 2026-03-10 12:46:27.022297835 +0100 +@@ -4932,6 +4932,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]) @@ -40,113 +9,10 @@ index d92a85809..2cbe20bf3 100644 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 03188d9b3..2c786ef14 100644 ---- a/gss-serv-krb5.c -+++ b/gss-serv-krb5.c -@@ -43,6 +43,7 @@ - #include "log.h" - #include "misc.h" - #include "servconf.h" -+#include "match.h" - - #include "ssh-gss.h" - -@@ -87,6 +88,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 -@@ -193,7 +220,7 @@ static int - ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) - { - krb5_principal princ; -- int retval; -+ int retval, matched; - const char *errmsg; - int k5login_exists; - -@@ -216,17 +243,42 @@ ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) - if (k5login_exists && - ssh_krb5_kuserok(krb_context, princ, name, k5login_exists)) { - retval = 1; -- logit("Authorized to %s, krb5 principal %s (krb5_kuserok)", -- name, (char *)client->displayname.value); -+ errmsg = "krb5_kuserok"; - } else if (ssh_gssapi_krb5_cmdok(princ, client->exportedname.value, - name, k5login_exists)) { - retval = 1; -- logit("Authorized to %s, krb5 principal %s " -- "(ssh_gssapi_krb5_cmdok)", -- name, (char *)client->displayname.value); -+ errmsg = "ssh_gssapi_krb5_cmdok"; - } 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 9d5435eda..5c0491cf1 100644 ---- a/gss-serv.c -+++ b/gss-serv.c -@@ -54,7 +54,7 @@ extern ServerOptions options; +diff --color -ruNp a/gss-serv.c b/gss-serv.c +--- a/gss-serv.c 2026-03-10 12:43:36.802443034 +0100 ++++ b/gss-serv.c 2026-03-12 10:04:37.520993330 +0100 +@@ -53,7 +53,7 @@ extern ServerOptions options; static ssh_gssapi_client gssapi_client = { GSS_C_EMPTY_BUFFER, GSS_C_EMPTY_BUFFER, GSS_C_NO_CREDENTIAL, @@ -155,7 +21,7 @@ index 9d5435eda..5c0491cf1 100644 ssh_gssapi_mech gssapi_null_mech = { NULL, NULL, {0, NULL}, NULL, NULL, NULL, NULL, NULL}; -@@ -296,6 +296,92 @@ ssh_gssapi_parse_ename(Gssctxt *ctx, gss_buffer_t ename, gss_buffer_t name) +@@ -295,6 +295,95 @@ ssh_gssapi_parse_ename(Gssctxt *ctx, gss return GSS_S_COMPLETE; } @@ -163,9 +29,10 @@ index 9d5435eda..5c0491cf1 100644 +/* 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. */ ++ * Each indicator is an utf8 string. */ + +#define AUTH_INDICATORS_TAG "auth-indicators" ++#define SSH_GSSAPI_MAX_INDICATORS 64 + +/* Privileged (called from accept_secure_ctx) */ +static OM_uint32 @@ -188,24 +55,7 @@ index 9d5435eda..5c0491cf1 100644 + 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*)); ++ client->indicators = NULL; + count = 0; + for (i = 0; i < attrs->count; i++) { + authenticated = 0; @@ -213,22 +63,33 @@ index 9d5435eda..5c0491cf1 100644 + 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) ++ 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) { ++ &attrs->elements[i], &authenticated, ++ &complete, &value, &display_value, &more); ++ if (ctx->major != GSS_S_COMPLETE) + goto out; -+ } + + if ((value.value != NULL) && authenticated) { ++ if (count >= SSH_GSSAPI_MAX_INDICATORS) { ++ logit("ssh_gssapi_getindicators: too many " ++ "indicators, truncating at %d", ++ SSH_GSSAPI_MAX_INDICATORS); ++ /* value/display_value released at out: */ ++ goto done; ++ } ++ ++ 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'; @@ -237,18 +98,26 @@ index 9d5435eda..5c0491cf1 100644 + } + } + ++done: ++ /* slot [count] is zeroed by recallocarray, serves as NULL sentinel */ ++ +out: ++ if (ctx->major != GSS_S_COMPLETE && client->indicators != NULL) { ++ for (i = 0; i < count; i++) ++ free(client->indicators[i]); ++ free(client->indicators); ++ client->indicators = NULL; ++ } + (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 */ -@@ -385,6 +471,12 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_gssapi_client *client) +@@ -384,6 +473,12 @@ ssh_gssapi_getclient(Gssctxt *ctx, ssh_g } gss_release_buffer(&ctx->minor, &ename); @@ -261,7 +130,7 @@ index 9d5435eda..5c0491cf1 100644 /* We can't copy this structure, so we just move the pointer to it */ client->creds = ctx->client_creds; -@@ -447,6 +539,7 @@ int +@@ -446,6 +541,7 @@ int ssh_gssapi_userok(char *user, struct passwd *pw, int kex) { OM_uint32 lmin; @@ -269,34 +138,174 @@ index 9d5435eda..5c0491cf1 100644 (void) kex; /* used in privilege separation */ -@@ -465,6 +558,14 @@ ssh_gssapi_userok(char *user, struct passwd *pw, int kex) +@@ -464,8 +560,14 @@ ssh_gssapi_userok(char *user, struct pas 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++) { ++ 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)); ++ explicit_bzero(&gssapi_client, sizeof(ssh_gssapi_client)); return 0; -diff --git a/servconf.c b/servconf.c -index e7e4ad046..aab653244 100644 ---- a/servconf.c -+++ b/servconf.c -@@ -147,6 +147,7 @@ initialize_server_options(ServerOptions *options) + } + else +diff --color -ruNp a/gss-serv-krb5.c b/gss-serv-krb5.c +--- a/gss-serv-krb5.c 2026-03-10 12:43:36.823015336 +0100 ++++ b/gss-serv-krb5.c 2026-03-11 12:58:56.024455238 +0100 +@@ -43,6 +43,7 @@ + #include "log.h" + #include "misc.h" + #include "servconf.h" ++#include "match.h" + + #include "ssh-gss.h" + +@@ -87,6 +88,33 @@ 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 +@@ -193,15 +221,15 @@ static int + ssh_gssapi_krb5_userok(ssh_gssapi_client *client, char *name) + { + krb5_principal princ; +- int retval; ++ int retval, matched, success; + const char *errmsg; + int k5login_exists; + + 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); +@@ -216,17 +244,60 @@ ssh_gssapi_krb5_userok(ssh_gssapi_client + if (k5login_exists && + ssh_krb5_kuserok(krb_context, princ, name, k5login_exists)) { + retval = 1; +- logit("Authorized to %s, krb5 principal %s (krb5_kuserok)", +- name, (char *)client->displayname.value); ++ errmsg = "krb5_kuserok"; + } else if (ssh_gssapi_krb5_cmdok(princ, client->exportedname.value, + name, k5login_exists)) { + retval = 1; +- logit("Authorized to %s, krb5 principal %s " +- "(ssh_gssapi_krb5_cmdok)", +- name, (char *)client->displayname.value); +- } else ++ errmsg = "ssh_gssapi_krb5_cmdok"; ++ } else { ++ retval = 0; ++ goto out; ++ } ++ ++ /* At this point we are good if no indicators were defined */ ++ if (options.gss_indicators == NULL) { ++ retval = 1; ++ 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 (%s)", ++ name, (char *)client->displayname.value, errmsg); ++ } + krb5_free_principal(krb_context, princ); + return retval; + } +diff --color -ruNp a/servconf.c b/servconf.c +--- a/servconf.c 2026-03-10 12:43:36.928060353 +0100 ++++ b/servconf.c 2026-03-11 13:20:09.725354925 +0100 +@@ -144,6 +144,7 @@ initialize_server_options(ServerOptions + options->gss_keyex = -1; + options->gss_cleanup_creds = -1; options->gss_strict_acceptor = -1; ++ options->gss_indicators = NULL; options->gss_store_rekey = -1; options->gss_kex_algorithms = NULL; -+ options->gss_indicators = NULL; options->use_kuserok = -1; - options->enable_k5users = -1; - options->password_authentication = -1; -@@ -598,7 +599,7 @@ typedef enum { +@@ -557,6 +558,7 @@ fill_default_server_options(ServerOption + 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]); +@@ -594,7 +596,7 @@ typedef enum { sPerSourcePenalties, sPerSourcePenaltyExemptList, sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile, sGssAuthentication, sGssCleanupCreds, sGssEnablek5users, sGssStrictAcceptor, @@ -305,7 +314,7 @@ index e7e4ad046..aab653244 100644 sAcceptEnv, sSetEnv, sPermitTunnel, sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, sUsePrivilegeSeparation, sAllowAgentForwarding, -@@ -694,6 +695,7 @@ static struct { +@@ -690,6 +692,7 @@ static struct { { "gssapistorecredentialsonrekey", sGssStoreRekey, SSHCFG_GLOBAL }, { "gssapikexalgorithms", sGssKexAlgorithms, SSHCFG_GLOBAL }, { "gssapienablek5users", sGssEnablek5users, SSHCFG_ALL }, @@ -313,7 +322,7 @@ index e7e4ad046..aab653244 100644 #else { "gssapiauthentication", sUnsupported, SSHCFG_ALL }, { "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL }, -@@ -703,6 +705,7 @@ static struct { +@@ -699,6 +702,7 @@ static struct { { "gssapistorecredentialsonrekey", sUnsupported, SSHCFG_GLOBAL }, { "gssapikexalgorithms", sUnsupported, SSHCFG_GLOBAL }, { "gssapienablek5users", sUnsupported, SSHCFG_ALL }, @@ -321,7 +330,7 @@ index e7e4ad046..aab653244 100644 #endif { "gssusesessionccache", sUnsupported, SSHCFG_GLOBAL }, { "gssapiusesessioncredcache", sUnsupported, SSHCFG_GLOBAL }, -@@ -1730,6 +1733,15 @@ process_server_config_line_depth(ServerOptions *options, char *line, +@@ -1715,6 +1719,15 @@ process_server_config_line_depth(ServerO options->gss_kex_algorithms = xstrdup(arg); break; @@ -337,7 +346,7 @@ index e7e4ad046..aab653244 100644 case sPasswordAuthentication: intptr = &options->password_authentication; goto parse_flag; -@@ -3351,6 +3363,7 @@ dump_config(ServerOptions *o) +@@ -3329,6 +3342,7 @@ dump_config(ServerOptions *o) dump_cfg_fmtint(sGssStrictAcceptor, o->gss_strict_acceptor); dump_cfg_fmtint(sGssStoreRekey, o->gss_store_rekey); dump_cfg_string(sGssKexAlgorithms, o->gss_kex_algorithms); @@ -345,10 +354,9 @@ index e7e4ad046..aab653244 100644 #endif dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication); dump_cfg_fmtint(sKbdInteractiveAuthentication, -diff --git a/servconf.h b/servconf.h -index 7c7e5d434..7c41df417 100644 ---- a/servconf.h -+++ b/servconf.h +diff --color -ruNp a/servconf.h b/servconf.h +--- a/servconf.h 2026-03-10 12:43:36.833119920 +0100 ++++ b/servconf.h 2026-03-11 13:21:36.742117033 +0100 @@ -181,6 +181,7 @@ typedef struct { char **allow_groups; u_int num_deny_groups; @@ -357,7 +365,7 @@ index 7c7e5d434..7c41df417 100644 u_int num_subsystems; char **subsystem_name; -@@ -310,6 +311,7 @@ TAILQ_HEAD(include_list, include_item); +@@ -309,6 +310,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); \ @@ -365,36 +373,10 @@ index 7c7e5d434..7c41df417 100644 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 a894e23c9..59cf46d47 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 -@@ -107,6 +113,7 @@ typedef struct { - ssh_gssapi_ccache store; - int used; - int updated; -+ char **indicators; /* auth indicators */ - } ssh_gssapi_client; - - typedef struct ssh_gssapi_mech_struct { -diff --git a/sshd_config.5 b/sshd_config.5 -index 583a01cdb..90ab87edd 100644 ---- a/sshd_config.5 -+++ b/sshd_config.5 -@@ -785,6 +785,50 @@ gss-nistp256-sha256- +diff --color -ruNp a/sshd_config.5 b/sshd_config.5 +--- a/sshd_config.5 2026-03-10 12:43:36.859313302 +0100 ++++ b/sshd_config.5 2026-03-11 13:28:04.541970063 +0100 +@@ -785,6 +785,52 @@ gss-nistp256-sha256- gss-curve25519-sha256- .Ed This option only applies to connections using GSSAPI. @@ -441,10 +423,33 @@ index 583a01cdb..90ab87edd 100644 +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. ++The default ++.Dq none ++is to not use GSSAPI authentication indicators for access decisions. .It Cm HostbasedAcceptedAlgorithms The default is handled system-wide by .Xr crypto-policies 7 . --- -2.49.0 - +diff --color -ruNp a/ssh-gss.h b/ssh-gss.h +--- a/ssh-gss.h 2026-03-10 12:43:36.898148309 +0100 ++++ b/ssh-gss.h 2026-03-11 13:23:07.601956965 +0100 +@@ -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 +@@ -112,6 +118,7 @@ typedef struct { + ssh_gssapi_ccache store; + int used; + int updated; ++ char **indicators; /* auth indicators */ + } ssh_gssapi_client; + + typedef struct ssh_gssapi_mech_struct { diff --git a/openssh.spec b/openssh.spec index 2c60682..0fbcc8c 100644 --- a/openssh.spec +++ b/openssh.spec @@ -727,6 +727,8 @@ test -f %{sysconfig_anaconda} && \ Resolves: RHEL-150365 - Fix mistracking of MaxStartups process exits in some situations Resolves: RHEL-121768 +- Fix GSSAPI authentication indicator issues found by AI + Resolves: RHEL-154309 * Wed Mar 11 2026 Dmitry Belyavskiy - 9.9p1-21 - Implement obtaining Kerberos tickets on behalf of user on SSH authentication