1047 lines
36 KiB
Diff
1047 lines
36 KiB
Diff
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)) {
|
|
+ /*
|
|
+ * For collection-type caches (KCM, KEYRING, …) reuse the
|
|
+ * existing primary ccache when one is already present. The
|
|
+ * caller will reinitialise it with krb5_cc_initialize(), so
|
|
+ * its old contents are replaced rather than orphaned. Only
|
|
+ * create a fresh unique ccache when no primary exists yet.
|
|
+ */
|
|
+ if (krb5_cc_default(ctx, ccache) == 0) {
|
|
+ debug3_f("reusing existing default ccache of type %s",
|
|
+ type);
|
|
+ free(type);
|
|
+ return 0;
|
|
+ }
|
|
debug3_f("calling cc_new_unique(%s)", ccname);
|
|
ret = krb5_cc_new_unique(ctx, type, NULL, ccache);
|
|
free(type);
|
|
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 <gssapi/gssapi_generic.h>
|
|
#endif
|
|
+#ifdef HAVE_GSSAPI_EXT_H
|
|
+# include <gssapi/gssapi_ext.h>
|
|
+#endif
|
|
+#ifdef HAVE_GSSAPI_KRB5_H
|
|
+# include <gssapi/gssapi_krb5.h>
|
|
+#endif
|
|
]])
|
|
saved_LIBS="$LIBS"
|
|
- LIBS="$LIBS $K5LIBS"
|
|
- AC_CHECK_FUNCS([krb5_cc_new_unique krb5_get_error_message krb5_free_error_message])
|
|
+ LIBS="$LIBS $GSSLIBS $K5LIBS "
|
|
+ AC_CHECK_FUNCS([krb5_cc_new_unique krb5_get_error_message krb5_free_error_message gss_acquire_cred_from])
|
|
LIBS="$saved_LIBS"
|
|
|
|
fi
|
|
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,382 @@ 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
|
|
+ {
|
|
+# if defined(KRB5)
|
|
+ /*
|
|
+ * Resolve the keytab path: krb5_kt_default_name respects
|
|
+ * KRB5_KTNAME and krb5.conf default_keytab_name.
|
|
+ */
|
|
+ char keytab_name[MAXPATHLEN];
|
|
+ krb5_context tmp_ctx;
|
|
+
|
|
+
|
|
+ keytab_name[0] = '\0';
|
|
+ if (krb5_init_context(&tmp_ctx) == 0) {
|
|
+ (void)krb5_kt_default_name(tmp_ctx, keytab_name,
|
|
+ sizeof(keytab_name));
|
|
+ krb5_free_context(tmp_ctx);
|
|
+ }
|
|
+ if (keytab_name[0] == '\0')
|
|
+ strlcpy(keytab_name, "FILE:/etc/krb5.keytab",
|
|
+ sizeof(keytab_name));
|
|
+ /*
|
|
+ * client_keytab lets GSSAPI do AS-REQ to obtain a TGT for the
|
|
+ * host principal (initiator role needed for S4U2Self).
|
|
+ * keytab covers the acceptor role.
|
|
+ * ccache: MEMORY: keeps the resulting TGT volatile.
|
|
+ */
|
|
+ gss_key_value_element_desc store_elements[] = {
|
|
+ { "client_keytab", keytab_name },
|
|
+ { "keytab", keytab_name },
|
|
+ { "ccache", "MEMORY:" },
|
|
+ };
|
|
+ const gss_key_value_set_desc cred_store = { 3, store_elements };
|
|
+# else
|
|
+ gss_key_value_element_desc store_elements[] = {
|
|
+ { "ccache", "MEMORY:" },
|
|
+ };
|
|
+ const gss_key_value_set_desc cred_store = { 1, store_elements };
|
|
+# endif
|
|
+
|
|
+
|
|
+ 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;
|
|
}
|
|
|
|
+/*
|
|
+ * Check whether the user's default ccache already contains valid service
|
|
+ * tickets for all principals listed in services[]. Returns 1 if every
|
|
+ * listed service has a ticket with at least min_lifetime seconds remaining
|
|
+ * (pass GSS_C_INDEFINITE to accept any positive remaining lifetime), 0 if
|
|
+ * any ticket is missing or too close to expiry. Runs as the user.
|
|
+ *
|
|
+ * Unlike ssh_gssapi_user_has_valid_tgt(), this cannot use gss_acquire_cred()
|
|
+ * because service tickets are not initiator credentials — GSSAPI only
|
|
+ * surfaces TGTs via that API. We iterate the ccache with the krb5 API
|
|
+ * directly instead.
|
|
+ */
|
|
+int
|
|
+ssh_gssapi_user_has_valid_proxy_tickets(char **services, u_int nservices,
|
|
+ u_int min_lifetime)
|
|
+{
|
|
+ krb5_context ctx = NULL;
|
|
+ krb5_ccache cc = NULL;
|
|
+ krb5_cc_cursor cursor;
|
|
+ krb5_creds cred;
|
|
+ krb5_principal svc_princ;
|
|
+ int *found = NULL;
|
|
+ u_int i;
|
|
+ int all_found = 0;
|
|
+ time_t now;
|
|
+
|
|
+ if (nservices == 0)
|
|
+ return 0;
|
|
+
|
|
+ if (krb5_init_context(&ctx) != 0)
|
|
+ return 0;
|
|
+
|
|
+ found = xcalloc(nservices, sizeof(*found));
|
|
+ now = time(NULL);
|
|
+
|
|
+ if (krb5_cc_default(ctx, &cc) != 0)
|
|
+ goto out;
|
|
+
|
|
+ if (krb5_cc_start_seq_get(ctx, cc, &cursor) != 0) {
|
|
+ krb5_cc_close(ctx, cc);
|
|
+ cc = NULL;
|
|
+ goto out;
|
|
+ }
|
|
+
|
|
+ while (krb5_cc_next_cred(ctx, cc, &cursor, &cred) == 0) {
|
|
+ /*
|
|
+ * Use krb5_principal_compare() so that service names
|
|
+ * configured without an explicit realm (krb5_parse_name
|
|
+ * appends the default realm) still match the fully-
|
|
+ * qualified principal stored in the ccache.
|
|
+ */
|
|
+ for (i = 0; i < nservices; i++) {
|
|
+ if (found[i])
|
|
+ continue;
|
|
+ if (krb5_parse_name(ctx, services[i],
|
|
+ &svc_princ) != 0)
|
|
+ continue;
|
|
+ if (krb5_principal_compare(ctx,
|
|
+ cred.server, svc_princ)) {
|
|
+ krb5_deltat remaining =
|
|
+ cred.times.endtime - now;
|
|
+ if (remaining > 0 &&
|
|
+ (min_lifetime == GSS_C_INDEFINITE ||
|
|
+ (krb5_deltat)min_lifetime <= remaining))
|
|
+ found[i] = 1;
|
|
+ }
|
|
+ krb5_free_principal(ctx, svc_princ);
|
|
+ }
|
|
+ krb5_free_cred_contents(ctx, &cred);
|
|
+ }
|
|
+ krb5_cc_end_seq_get(ctx, cc, &cursor);
|
|
+ krb5_cc_close(ctx, cc);
|
|
+ cc = NULL;
|
|
+
|
|
+ all_found = 1;
|
|
+ for (i = 0; i < nservices; i++) {
|
|
+ if (!found[i]) {
|
|
+ all_found = 0;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+out:
|
|
+ free(found);
|
|
+ if (ctx != NULL)
|
|
+ krb5_free_context(ctx);
|
|
+ return all_found;
|
|
+}
|
|
+
|
|
+/* As user - called on fatal/exit */
|
|
+void
|
|
+ssh_gssapi_cleanup_creds(void)
|
|
+{
|
|
+ ssh_gssapi_ccache *store = ssh_gssapi_get_ccache();
|
|
+ krb5_ccache ccache = NULL;
|
|
+ krb5_error_code problem;
|
|
+
|
|
+ if (store->data != NULL) {
|
|
+ if ((problem = krb5_cc_resolve(store->data,
|
|
+ store->envval, &ccache))) {
|
|
+ debug_f("krb5_cc_resolve(): %.100s",
|
|
+ krb5_get_err_text(store->data, problem));
|
|
+ } else if ((problem = krb5_cc_destroy(store->data, ccache))) {
|
|
+ debug_f("krb5_cc_destroy(): %.100s",
|
|
+ krb5_get_err_text(store->data, problem));
|
|
+ } else {
|
|
+ krb5_free_context(store->data);
|
|
+ store->data = NULL;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+/*
|
|
+ * Filter the user's ccache by removing the ticket classes indicated by
|
|
+ * drop_flags (SSH_GSSAPI_CCFILTER_* bitmask). Each credential is
|
|
+ * categorised as one of:
|
|
+ * TGT - server principal matches "krbtgt/" prefix
|
|
+ * PROXY - server principal matches one of proxy_services[]
|
|
+ * SELF - everything else (the S4U2Self evidence ticket)
|
|
+ * Credentials in a flagged category are discarded; the rest are written
|
|
+ * back after reinitialising the ccache. Runs as user.
|
|
+ */
|
|
+void
|
|
+ssh_gssapi_krb5_filter_ccache(u_int drop_flags,
|
|
+ char **proxy_services, u_int nproxy_services)
|
|
+{
|
|
+ ssh_gssapi_ccache *store = ssh_gssapi_get_ccache();
|
|
+ krb5_context ctx = (krb5_context)store->data;
|
|
+ krb5_ccache cc = NULL;
|
|
+ krb5_cc_cursor cursor;
|
|
+ krb5_creds *keep = NULL;
|
|
+ krb5_principal princ = NULL;
|
|
+ krb5_error_code problem;
|
|
+ char *srvname;
|
|
+ u_int i, nkeep = 0, cap = 0;
|
|
+ int is_tgt, is_proxy, drop;
|
|
+
|
|
+ if (ctx == NULL || store->envval == NULL)
|
|
+ return;
|
|
+
|
|
+ if ((problem = krb5_cc_resolve(ctx, store->envval, &cc)) != 0) {
|
|
+ debug_f("krb5_cc_resolve: %.100s",
|
|
+ krb5_get_err_text(ctx, problem));
|
|
+ return;
|
|
+ }
|
|
+ if ((problem = krb5_cc_get_principal(ctx, cc, &princ)) != 0) {
|
|
+ debug_f("krb5_cc_get_principal: %.100s",
|
|
+ krb5_get_err_text(ctx, problem));
|
|
+ krb5_cc_close(ctx, cc);
|
|
+ return;
|
|
+ }
|
|
+ if ((problem = krb5_cc_start_seq_get(ctx, cc, &cursor)) != 0) {
|
|
+ debug_f("krb5_cc_start_seq_get: %.100s",
|
|
+ krb5_get_err_text(ctx, problem));
|
|
+ krb5_free_principal(ctx, princ);
|
|
+ krb5_cc_close(ctx, cc);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ {
|
|
+ krb5_creds cred;
|
|
+ krb5_principal svc_princ;
|
|
+ while (krb5_cc_next_cred(ctx, cc, &cursor, &cred) == 0) {
|
|
+ is_tgt = is_proxy = 0;
|
|
+ if (krb5_unparse_name(ctx, cred.server,
|
|
+ &srvname) == 0) {
|
|
+ is_tgt = strncmp(srvname, "krbtgt/", 7) == 0;
|
|
+ krb5_free_unparsed_name(ctx, srvname);
|
|
+ }
|
|
+ if (!is_tgt) {
|
|
+ /*
|
|
+ * Use krb5_principal_compare() rather than
|
|
+ * strcmp() so that a service name configured
|
|
+ * without an explicit realm (krb5_parse_name
|
|
+ * appends the default realm) still matches
|
|
+ * the fully-qualified name in the ccache.
|
|
+ */
|
|
+ for (i = 0; i < nproxy_services; i++) {
|
|
+ if (krb5_parse_name(ctx,
|
|
+ proxy_services[i], &svc_princ) != 0)
|
|
+ continue;
|
|
+ if (krb5_principal_compare(ctx,
|
|
+ cred.server, svc_princ))
|
|
+ is_proxy = 1;
|
|
+ krb5_free_principal(ctx, svc_princ);
|
|
+ if (is_proxy)
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ if (is_tgt)
|
|
+ drop = drop_flags & SSH_GSSAPI_CCFILTER_TGT;
|
|
+ else if (is_proxy)
|
|
+ drop = drop_flags & SSH_GSSAPI_CCFILTER_PROXY;
|
|
+ else
|
|
+ drop = drop_flags & SSH_GSSAPI_CCFILTER_SELF;
|
|
+
|
|
+ if (!drop) {
|
|
+ if (nkeep >= cap) {
|
|
+ cap = cap ? cap * 2 : 4;
|
|
+ keep = xreallocarray(keep, cap,
|
|
+ sizeof(*keep));
|
|
+ }
|
|
+ keep[nkeep++] = cred;
|
|
+ } else
|
|
+ krb5_free_cred_contents(ctx, &cred);
|
|
+ }
|
|
+ }
|
|
+ krb5_cc_end_seq_get(ctx, cc, &cursor);
|
|
+
|
|
+ if ((problem = krb5_cc_initialize(ctx, cc, princ)) != 0) {
|
|
+ logit_f("krb5_cc_initialize: %.100s",
|
|
+ krb5_get_err_text(ctx, problem));
|
|
+ } else {
|
|
+ for (i = 0; i < nkeep; i++)
|
|
+ krb5_cc_store_cred(ctx, cc, &keep[i]);
|
|
+ debug_f("ccache filter 0x%x: retained %u ticket(s)",
|
|
+ drop_flags, nkeep);
|
|
+ }
|
|
+
|
|
+ for (i = 0; i < nkeep; i++)
|
|
+ krb5_free_cred_contents(ctx, &keep[i]);
|
|
+ free(keep);
|
|
+ krb5_free_principal(ctx, princ);
|
|
+ krb5_cc_close(ctx, cc);
|
|
+}
|
|
+
|
|
ssh_gssapi_mech gssapi_kerberos_mech = {
|
|
"toWM5Slw5Ew8Mqkay+al2g==",
|
|
"Kerberos",
|
|
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_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;
|
|
@@ -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;
|
|
+ if (options->gss_allow_s4u2self == -1)
|
|
+ options->gss_allow_s4u2self = 0;
|
|
if (options->gss_store_rekey == -1)
|
|
options->gss_store_rekey = 0;
|
|
#ifdef GSSAPI
|
|
@@ -603,7 +608,7 @@ typedef enum {
|
|
sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile,
|
|
sGssAuthentication, sGssCleanupCreds, sGssDelegateCreds,
|
|
sGssEnablek5users, sGssStrictAcceptor,
|
|
- sGssKeyEx, sGssIndicators, sGssKexAlgorithms, sGssStoreRekey,
|
|
+ sGssKeyEx, sGssIndicators, sGssAllowS4U2Self, sGssProxyS4U2Services, sGssKexAlgorithms, sGssStoreRekey,
|
|
sAcceptEnv, sSetEnv, sPermitTunnel,
|
|
sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory,
|
|
sUsePrivilegeSeparation, sAllowAgentForwarding,
|
|
@@ -701,6 +706,8 @@ static struct {
|
|
{ "gssapikexalgorithms", sGssKexAlgorithms, SSHCFG_GLOBAL },
|
|
{ "gssapienablek5users", sGssEnablek5users, SSHCFG_ALL },
|
|
{ "gssapiindicators", sGssIndicators, SSHCFG_ALL },
|
|
+ { "gssapiallows4u2self", sGssAllowS4U2Self, SSHCFG_ALL },
|
|
+ { "gssapiproxys4u2services", sGssProxyS4U2Services, SSHCFG_ALL },
|
|
#else
|
|
{ "gssapiauthentication", sUnsupported, SSHCFG_ALL },
|
|
{ "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL },
|
|
@@ -712,6 +719,8 @@ static struct {
|
|
{ "gssapikexalgorithms", sUnsupported, SSHCFG_GLOBAL },
|
|
{ "gssapienablek5users", sUnsupported, SSHCFG_ALL },
|
|
{ "gssapiindicators", sUnsupported, SSHCFG_ALL },
|
|
+ { "gssapiallows4u2self", sUnsupported, SSHCFG_ALL },
|
|
+ { "gssapiproxys4u2services", sUnsupported, SSHCFG_ALL },
|
|
#endif
|
|
{ "gssusesessionccache", sUnsupported, SSHCFG_GLOBAL },
|
|
{ "gssapiusesessioncredcache", sUnsupported, SSHCFG_GLOBAL },
|
|
@@ -1742,6 +1751,44 @@ process_server_config_line_depth(ServerO
|
|
options->gss_indicators = xstrdup(arg);
|
|
break;
|
|
|
|
+ case sGssAllowS4U2Self:
|
|
+ arg = argv_next(&ac, &av);
|
|
+ if (!arg || *arg == '\0')
|
|
+ fatal("%s line %d: %s missing argument.",
|
|
+ filename, linenum, keyword);
|
|
+ if (strcasecmp(arg, "no") == 0)
|
|
+ value = 0;
|
|
+ else if (strcasecmp(arg, "yes") == 0)
|
|
+ value = INT_MAX;
|
|
+ else if ((value = convtime(arg)) <= 0)
|
|
+ fatal("%s line %d: invalid %s value \"%s\".",
|
|
+ filename, linenum, keyword, arg);
|
|
+ if (*activep && options->gss_allow_s4u2self == -1)
|
|
+ options->gss_allow_s4u2self = value;
|
|
+ break;
|
|
+
|
|
+ case sGssProxyS4U2Services:
|
|
+ while ((arg = argv_next(&ac, &av)) != NULL) {
|
|
+ if (*arg == '\0')
|
|
+ fatal("%s line %d: %s missing argument.",
|
|
+ filename, linenum, keyword);
|
|
+ if (strcasecmp(arg, "none") == 0) {
|
|
+ /* "none" clears any previous list */
|
|
+ for (i = 0; i < options->num_gss_proxy_services; i++)
|
|
+ free(options->gss_proxy_services[i]);
|
|
+ free(options->gss_proxy_services);
|
|
+ options->gss_proxy_services = NULL;
|
|
+ options->num_gss_proxy_services = 0;
|
|
+ break;
|
|
+ }
|
|
+ if (!*activep)
|
|
+ continue;
|
|
+ opt_array_append(filename, linenum, keyword,
|
|
+ &options->gss_proxy_services,
|
|
+ &options->num_gss_proxy_services, arg);
|
|
+ }
|
|
+ break;
|
|
+
|
|
case sPasswordAuthentication:
|
|
intptr = &options->password_authentication;
|
|
goto parse_flag;
|
|
@@ -3010,6 +3057,7 @@ copy_set_server_options(ServerOptions *d
|
|
|
|
M_CP_INTOPT(password_authentication);
|
|
M_CP_INTOPT(gss_authentication);
|
|
+ M_CP_INTOPT(gss_allow_s4u2self);
|
|
M_CP_INTOPT(pubkey_authentication);
|
|
M_CP_INTOPT(pubkey_auth_options);
|
|
M_CP_INTOPT(kerberos_authentication);
|
|
@@ -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);
|
|
+ if (o->gss_allow_s4u2self == 0)
|
|
+ printf("%s no\n", lookup_opcode_name(sGssAllowS4U2Self));
|
|
+ else if (o->gss_allow_s4u2self == INT_MAX)
|
|
+ printf("%s yes\n", lookup_opcode_name(sGssAllowS4U2Self));
|
|
+ else
|
|
+ printf("%s %d\n", lookup_opcode_name(sGssAllowS4U2Self),
|
|
+ o->gss_allow_s4u2self);
|
|
+ dump_cfg_strarray_oneline(sGssProxyS4U2Services, o->num_gss_proxy_services,
|
|
+ o->gss_proxy_services);
|
|
#endif
|
|
dump_cfg_fmtint(sPasswordAuthentication, o->password_authentication);
|
|
dump_cfg_fmtint(sKbdInteractiveAuthentication,
|
|
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 */
|
|
int gss_strict_acceptor; /* If true, restrict the GSSAPI acceptor name */
|
|
+ int gss_allow_s4u2self; /* 0=no, INT_MAX=yes (GSS_C_INDEFINITE), >0=ticket lifetime s */
|
|
+ char **gss_proxy_services; /* S4U2Proxy target service principals */
|
|
+ u_int num_gss_proxy_services;
|
|
int gss_store_rekey;
|
|
char *gss_kex_algorithms; /* GSSAPI kex methods to be offered by client. */
|
|
int password_authentication; /* If true, permit password
|
|
@@ -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); \
|
|
+ M_CP_STRARRAYOPT(gss_proxy_services, num_gss_proxy_services); \
|
|
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 --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 GSSAPIProxyS4U2Services
|
|
+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();
|
|
}
|
|
+ /*
|
|
+ * GSSAPIAllowS4U2Self / GSSAPIProxyS4U2Services: if no credentials were stored
|
|
+ * above (i.e. no GSSAPI auth with delegation occurred), use S4U2Self
|
|
+ * to obtain an impersonated credential for the user, then optionally
|
|
+ * follow with S4U2Proxy for configured target services.
|
|
+ *
|
|
+ * GSSAPIAllowS4U2Self alone: store S4U2Self evidence ticket only;
|
|
+ * the host TGT is removed.
|
|
+ * GSSAPIProxyS4U2Services alone: store host TGT and S4U2Proxy service
|
|
+ * tickets; the S4U2Self evidence ticket
|
|
+ * is removed.
|
|
+ * Both: store host TGT, S4U2Self evidence ticket,
|
|
+ * and all S4U2Proxy service tickets.
|
|
+ *
|
|
+ * When S4U2Proxy tickets are present the host TGT must remain in the
|
|
+ * ccache; applications check for TGT presence to determine whether
|
|
+ * Kerberos credentials are available. Only in GSSAPIAllowS4U2Self-alone
|
|
+ * mode (no proxy tickets) is the host TGT removed.
|
|
+ *
|
|
+ * Skip S4U2Self when the user already has credentials covering the
|
|
+ * requested lifetime: check for a valid TGT in the GSSAPIAllowS4U2Self-
|
|
+ * alone case, or for valid proxy tickets for every configured service
|
|
+ * otherwise.
|
|
+ */
|
|
+ if ((options.gss_allow_s4u2self || options.num_gss_proxy_services > 0) &&
|
|
+ !ssh_gssapi_credentials_stored()) {
|
|
+ u_int lifetime = (!options.gss_allow_s4u2self ||
|
|
+ options.gss_allow_s4u2self == INT_MAX) ?
|
|
+ GSS_C_INDEFINITE : (u_int)options.gss_allow_s4u2self;
|
|
+ int skip = 0;
|
|
+
|
|
+ temporarily_use_uid(authctxt->pw);
|
|
+ if (options.gss_allow_s4u2self &&
|
|
+ options.num_gss_proxy_services == 0) {
|
|
+ /* S4U2Self-alone: skip if user already has a valid TGT */
|
|
+ skip = ssh_gssapi_user_has_valid_tgt(lifetime);
|
|
+ } else if (options.num_gss_proxy_services > 0) {
|
|
+ /*
|
|
+ * Proxy-only or both: skip if every configured service
|
|
+ * already has a valid ticket in the user's ccache.
|
|
+ * Service tickets are not GSSAPI initiator credentials,
|
|
+ * so gss_acquire_cred() cannot be used; iterate the
|
|
+ * ccache with the krb5 API instead.
|
|
+ */
|
|
+ skip = ssh_gssapi_user_has_valid_proxy_tickets(
|
|
+ options.gss_proxy_services,
|
|
+ options.num_gss_proxy_services,
|
|
+ lifetime);
|
|
+ }
|
|
+ restore_uid();
|
|
+
|
|
+ if (skip) {
|
|
+ debug_f("user %.100s already has valid Kerberos "
|
|
+ "credentials, skipping S4U2Self",
|
|
+ authctxt->user);
|
|
+ } else if (ssh_gssapi_s4u2self(authctxt->user, lifetime) == 0) {
|
|
+ u_int filter;
|
|
+
|
|
+ temporarily_use_uid(authctxt->pw);
|
|
+ /*
|
|
+ * Always create the ccache via storecreds_s4u2self so
|
|
+ * that s4u2proxy has a ccache to store tickets into.
|
|
+ * gss_krb5_copy_ccache() copies the host service's own
|
|
+ * TGT along with the evidence ticket; filter_ccache
|
|
+ * removes the ticket classes that should not be kept.
|
|
+ */
|
|
+ ssh_gssapi_storecreds_s4u2self();
|
|
+ if (options.num_gss_proxy_services > 0)
|
|
+ ssh_gssapi_s4u2proxy(
|
|
+ options.gss_proxy_services,
|
|
+ options.num_gss_proxy_services,
|
|
+ lifetime);
|
|
+
|
|
+ /*
|
|
+ * Remove the host TGT only in GSSAPIAllowS4U2Self-alone
|
|
+ * mode; when proxy tickets are present the TGT must
|
|
+ * stay so that applications recognise the ccache as
|
|
+ * holding live Kerberos credentials.
|
|
+ * Remove the S4U2Self evidence ticket in proxy-only
|
|
+ * mode (GSSAPIProxyS4U2Services without GSSAPIAllowS4U2Self).
|
|
+ */
|
|
+ filter = 0;
|
|
+ if (options.gss_allow_s4u2self &&
|
|
+ options.num_gss_proxy_services == 0)
|
|
+ filter = SSH_GSSAPI_CCFILTER_TGT |
|
|
+ SSH_GSSAPI_CCFILTER_PROXY;
|
|
+ else if (!options.gss_allow_s4u2self)
|
|
+ filter = SSH_GSSAPI_CCFILTER_SELF;
|
|
+ if (filter != 0)
|
|
+ ssh_gssapi_krb5_filter_ccache(filter,
|
|
+ options.gss_proxy_services,
|
|
+ options.num_gss_proxy_services);
|
|
+ restore_uid();
|
|
+ } else {
|
|
+ logit("S4U2Self failed for user %.100s, continuing",
|
|
+ authctxt->user);
|
|
+ }
|
|
+ }
|
|
#endif
|
|
#ifdef WITH_SELINUX
|
|
sshd_selinux_setup_exec_context(authctxt->pw->pw_name,
|
|
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);
|