[openssh-commits] [openssh] 04/14: upstream: Allow specification of agent socket directories

git+noreply at mindrot.org git+noreply at mindrot.org
Wed Sep 16 11:15:58 AEST 2026


This is an automated email from the git hooks/post-receive script.

djm pushed a commit to branch master
in repository openssh.

commit cee6aedb1a176959164ab92556402e316469b8fb
Author: djm at openbsd.org <djm at openbsd.org>
AuthorDate: Wed Sep 16 00:25:50 2026 +0000

    upstream: Allow specification of agent socket directories
    
    Add AgentSocketPath for sshd_config and -A flag for ssh-agent.
    
    Agent socket directories may be shared or user-specific.
    
    Shared directories (specified like "shared:/tmp") will cause the listening
    program to create a temporary subdirectory ssh-XXXXXXXXXX under the requeted
    path to hold the socket. This supports the old sshd/ssh-agent behaviour before
    we switched to the socket directory being under ~/.ssh/agent
    
    User-specific directories just create the socket directly in the requested
    directory. This is the default, as user:.ssh/agent
    
    bz3860; ok markus, deraadt
    
    OpenBSD-Commit-ID: 91b95d02f376ad8959ffb23902cc115a0b74d9bf
---
 misc-agent.c  | 238 ++++++++++++++++++++++++++++++++++++++++++++--------------
 misc.h        |  10 ++-
 pathnames.h   |   9 ++-
 servconf.c    |  29 ++++++-
 servconf.h    |   6 +-
 session.c     |  11 ++-
 ssh-agent.1   |  39 ++++++++--
 ssh-agent.c   |  77 ++++++++++---------
 sshd_config.5 |  30 +++++++-
 9 files changed, 335 insertions(+), 114 deletions(-)

diff --git a/misc-agent.c b/misc-agent.c
index cb61405a7..eaf08b079 100644
--- a/misc-agent.c
+++ b/misc-agent.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc-agent.c,v 1.7 2026/02/11 17:05:32 dtucker Exp $ */
+/* $OpenBSD: misc-agent.c,v 1.8 2026/09/16 00:25:50 djm Exp $ */
 /*
  * Copyright (c) 2025 Damien Miller <djm at mindrot.org>
  *
@@ -30,6 +30,7 @@
 #include <string.h>
 #include <time.h>
 #include <unistd.h>
+#include <libgen.h>
 
 #include "digest.h"
 #include "log.h"
@@ -78,7 +79,7 @@ hostname_hash(size_t len)
 	return xstrdup(p);
 }
 
-char *
+static char *
 agent_hostname_hash(void)
 {
 	return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
@@ -154,71 +155,188 @@ unix_listener_tmp(char *path, int backlog)
 }
 
 /*
- * Create a subdirectory under the supplied home directory if it
- * doesn't already exist
+ * Shared directory case (e.g. /tmp): create a temporary directory
+ * for the socket.
  */
 static int
-ensure_mkdir(const char *homedir, const char *subdir)
+agent_listener_shared(const char *parent_dir, pid_t pid, const char *tag,
+    int *sockp, char **pathp, char **dirp)
 {
-	char *path;
+	char *dir = NULL, *path = NULL;
+	int sock, ret = -1;
+	mode_t prev_mask;
 
-	xasprintf(&path, "%s/%s", homedir, subdir);
-	if (mkdir(path, 0700) == 0)
-		debug("created directory %s", path);
-	else if (errno != EEXIST) {
-		error_f("mkdir %s: %s", path, strerror(errno));
-		free(path);
-		return -1;
+	*pathp = *dirp = NULL;
+	xasprintf(&dir, "%s/ssh-XXXXXXXXXXXX", parent_dir);
+	if (mkdtemp(dir) == NULL) {
+		error_f("failed to create temporary directory "
+		    "in \"%s\": %s", dir, strerror(errno));
+		goto out;
 	}
+	xasprintf(&path, "%s/agent.%s.%ld", dir, tag, (long)pid);
+	prev_mask = umask(0177);
+	if ((sock = unix_listener(path, SSH_LISTEN_BACKLOG, 0)) < 0) {
+		/* Error already logged */
+		umask(prev_mask);
+		if (rmdir(dir) != 0)
+			error_f("rmdir \"%s\": %s", dir, strerror(errno));
+		goto out;
+	}
+	umask(prev_mask);
+
+	/* Success */
+	*dirp = dir;
+	dir = NULL; /* transferred */
+	*pathp = path;
+	path = NULL; /* transferred */
+	*sockp = sock;
+	ret = 0;
+ out:
+	free(dir);
 	free(path);
-	return 0;
+	return ret;
 }
 
+/*
+ * User-specific directory case (e.g. ~/.ssh/agent): ensure directory
+ * exists, and use a temp socket name under it.
+ */
 static int
-agent_prepare_sockdir(const char *homedir)
+agent_listener_user(const char *dir, pid_t pid, const char *tag,
+    int *sockp, char **pathp)
 {
-	if (homedir == NULL || *homedir == '\0' ||
-	    ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 ||
-	    ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0)
-		return -1;
-	return 0;
-}
-
-
-/* Get a path template for an agent socket in the user's homedir */
-static char *
-agent_socket_template(const char *homedir, const char *tag)
-{
-	char *hostnamehash, *ret;
+	char *hostnamehash = NULL, *path = NULL;
+	int sock, ret = -1;
 
 	if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL)
-		return NULL;
-	xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX",
-	    homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag);
+		return -1;
+	xasprintf(&path, "%s/s.%s.%s.%lld.XXXXXXXXXX",
+	    dir, hostnamehash, tag, (long long)pid);
+	if (mkdir_path(dir, 0700) != 0) {
+		error_f("failed to create agent socket parent directory");
+		goto out;
+	}
+	if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
+		/* error already logged */
+		goto out;
+	}
+	/* Success */
+	*pathp = path;
+	path = NULL; /* transferred */
+	*sockp = sock;
+	ret = 0;
+ out:
 	free(hostnamehash);
+	free(path);
 	return ret;
 }
 
+static char *
+expand_pathspec(const char *path, const char *username,
+    uid_t uid, const char *homedir)
+{
+	char *uidbuf = NULL, *dir = NULL, *tmp = NULL;
+
+	xasprintf(&uidbuf, "%lld", (long long)uid);
+
+	if ((tmp = percent_expand(path, "u", username, "U", uidbuf,
+	    "h", homedir, NULL)) == NULL) {
+		error_f("failed to percent-expand agent socket directory");
+		goto out;
+	}
+	if (tilde_expand(tmp, uid, &dir) != 0) {
+		error_f("failed to user-expand agent socket directory");
+		goto out;
+	}
+	if (dir[0] != '/') {
+		/* Assume it's relative to the home directory */
+		free(tmp);
+		tmp = dir;
+		xasprintf(&dir, "%s/%s", homedir, tmp);
+	}
+ out:
+	free(uidbuf);
+	free(tmp);
+	return dir;
+}
+
 int
-agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp)
+agent_listener(const char *pathspec, const char *username, uid_t uid,
+    const char *homedir, pid_t pid, const char *tag, int *sockp,
+    char **pathp, char **dirp)
 {
-	int sock;
-	char *path;
+	int sock = -1, ret = -1;
+	char *path = NULL, *dir = NULL;
 
 	*sockp = -1;
-	*pathp = NULL;
+	*pathp = *dirp = NULL;
 
-	if (agent_prepare_sockdir(homedir) != 0)
-		return -1; /* error already logged */
-	if ((path = agent_socket_template(homedir, tag)) == NULL)
-		return -1; /* error already logged */
-	if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
-		free(path);
-		return -1; /* error already logged */
+	if (pathspec == NULL || pathspec[0] == '\0') {
+		error_f("no agent path specified");
+		return -1;
 	}
+	if (strncmp(pathspec, "shared:", 7) == 0) {
+		if (pathspec[7] != '/') {
+			error_f("shared agent socket paths must be absoute");
+			goto out;
+		}
+		if ((dir = expand_pathspec(pathspec + 7,
+		    username, uid, homedir)) == NULL) {
+			/* Error already logged */
+			goto out;
+		}
+		if (agent_listener_shared(dir, pid, tag,
+		    &sock, &path, dirp) != 0) {
+			/* Error already logged */
+			goto out;
+		}
+	} else if (strncmp(pathspec, "user:", 5) == 0) {
+		if ((dir = expand_pathspec(pathspec + 5,
+		    username, uid, homedir)) == NULL) {
+			/* Error already logged */
+			goto out;
+		}
+		if (agent_listener_user(dir, pid, tag, &sock, &path) != 0) {
+			/* Error already logged */
+			goto out;
+		}
+	} else {
+		/* Shouldn't happen */
+		error_f("unsupported agent path specification %s", pathspec);
+		goto out;
+	}
+
 	/* success */
+	ret = 0;
 	*sockp = sock;
 	*pathp = path;
+	path = NULL; /* transferred */
+ out:
+	free(path);
+	free(dir);
+	return ret;
+}
+
+int
+agent_listener_cleanup(const char *pathspec, const char *sockpath,
+    const char *sockdir)
+{
+	if (sockpath == NULL || pathspec == NULL)
+		return 0;
+	if (unlink(sockpath) != 0) {
+		error_f("unlink \"%s\": %s", sockpath, strerror(errno));
+		return -1;
+	}
+	debug3_f("removed socket %s", sockpath);
+
+	if (strncmp(pathspec, "shared:", 7) == 0 && sockdir != NULL) {
+		if (rmdir(sockdir) != 0) {
+			error_f("rmdir \"%s\": %s", sockdir, strerror(errno));
+			return -1;
+		}
+		debug3_f("removed socket directory %s", sockdir);
+	}
+
 	return 0;
 }
 
@@ -271,14 +389,25 @@ socket_is_stale(const char *path)
 #endif
 
 void
-agent_cleanup_stale(const char *homedir, int ignore_hosthash)
+agent_cleanup_stale(const char *pathspec, const char *username, uid_t uid,
+    const char *homedir, int ignore_hosthash)
 {
 	DIR *d = NULL;
 	struct dirent *dp;
 	struct stat sb;
-	char *prefix = NULL, *dirpath = NULL, *path = NULL;
+	char *prefix = NULL, *dir = NULL, *path;
 	struct timespec now, sub, *mtimp = NULL;
 
+	/* Only clean up user socket directories */
+	if (pathspec == NULL || strncmp(pathspec, "user:", 5) != 0)
+		return;
+
+	if ((dir = expand_pathspec(pathspec + 5,
+	    username, uid, homedir)) == NULL)
+		return; /* error already logged */
+
+	debug_f("cleanup %s", dir);
+
 	/* Only consider sockets last modified > 1 hour ago */
 	if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
 		error_f("clock_gettime: %s", strerror(errno));
@@ -296,20 +425,14 @@ agent_cleanup_stale(const char *homedir, int ignore_hosthash)
 		}
 		xasprintf(&prefix, "s.%s.", path);
 		free(path);
-		path = NULL;
 	}
 
-	xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR);
-	if ((d = opendir(dirpath)) == NULL) {
+	if ((d = opendir(dir)) == NULL) {
 		if (errno != ENOENT)
-			error_f("opendir \"%s\": %s", dirpath, strerror(errno));
+			error_f("opendir \"%s\": %s", dir, strerror(errno));
 		goto out;
 	}
-
-	path = NULL;
 	while ((dp = readdir(d)) != NULL) {
-		free(path);
-		xasprintf(&path, "%s/%s", dirpath, dp->d_name);
 #ifdef HAVE_DIRENT_D_TYPE
 		if (dp->d_type != DT_SOCK && dp->d_type != DT_UNKNOWN)
 			continue;
@@ -317,7 +440,7 @@ agent_cleanup_stale(const char *homedir, int ignore_hosthash)
 		if (fstatat(dirfd(d), dp->d_name,
 		    &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) {
 			error_f("stat \"%s/%s\": %s",
-			    dirpath, dp->d_name, strerror(errno));
+			    dir, dp->d_name, strerror(errno));
 			continue;
 		}
 		if (!S_ISSOCK(sb.st_mode))
@@ -331,25 +454,26 @@ agent_cleanup_stale(const char *homedir, int ignore_hosthash)
 #endif
 		if (timespeccmp(mtimp, &now, >)) {
 			debug3_f("Ignoring recent socket \"%s/%s\"",
-			    dirpath, dp->d_name);
+			    dir, dp->d_name);
 			continue;
 		}
 		if (!ignore_hosthash &&
 		    strncmp(dp->d_name, prefix, strlen(prefix)) != 0) {
 			debug3_f("Ignoring socket \"%s/%s\" "
-			    "from different host", dirpath, dp->d_name);
+			    "from different host", dir, dp->d_name);
 			continue;
 		}
+		xasprintf(&path, "%s/%s", dir, dp->d_name);
 		if (socket_is_stale(path)) {
 			debug_f("cleanup stale socket %s", path);
 			unlinkat(dirfd(d), dp->d_name, 0);
 		}
+		free(path);
 	}
  out:
 	if (d != NULL)
 		closedir(d);
-	free(path);
-	free(dirpath);
+	free(dir);
 	free(prefix);
 }
 
diff --git a/misc.h b/misc.h
index 87899a90d..589213f21 100644
--- a/misc.h
+++ b/misc.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc.h,v 1.118 2026/09/16 00:13:58 djm Exp $ */
+/* $OpenBSD: misc.h,v 1.119 2026/09/16 00:25:50 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -241,9 +241,11 @@ struct timespec *ptimeout_get_tsp(struct timespec *pt);
 int ptimeout_isset(struct timespec *pt);
 
 /* misc-agent.c */
-char	*agent_hostname_hash(void);
-int	 agent_listener(const char *, const char *, int *, char **);
-void	 agent_cleanup_stale(const char *, int);
+int	 agent_listener(const char *, const char *, uid_t, const char *,
+	    pid_t, const char *, int *, char **, char **);
+void	 agent_cleanup_stale(const char *, const char *, uid_t,
+	    const char *, int);
+int	 agent_listener_cleanup(const char *, const char *, const char *);
 
 /* readpass.c */
 
diff --git a/pathnames.h b/pathnames.h
index 2f0b9f4f6..8713153dc 100644
--- a/pathnames.h
+++ b/pathnames.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: pathnames.h,v 1.37 2026/06/14 03:59:34 djm Exp $ */
+/* $OpenBSD: pathnames.h,v 1.38 2026/09/16 00:25:50 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -68,10 +68,13 @@
 
 
 /*
- * The directory in which ssh-agent sockets and agent sockets forwarded by
+ * Directory spec for ssh-agent sockets and agent sockets forwarded by
  * sshd reside. This directory should not be world-readable.
  */
-#define _PATH_SSH_AGENT_SOCKET_DIR _PATH_SSH_USER_DIR "/agent"
+#define _PATH_SSH_AGENT_SOCKET_DIR	"user:" _PATH_SSH_USER_DIR "/agent"
+
+/* Directory spec for ssh-agent sockets in /tmp */
+#define _PATH_SSH_AGENT_SOCKET_TMPDIR	"shared:/tmp"
 
 /*
  * Per-user file containing host keys of known hosts.  This file need not be
diff --git a/servconf.c b/servconf.c
index a5bf3479a..becf72b81 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.c,v 1.454 2026/09/16 00:16:52 djm Exp $ */
+/* $OpenBSD: servconf.c,v 1.455 2026/09/16 00:25:50 djm Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -414,6 +414,8 @@ fill_default_server_options(ServerOptions *options)
 		options->pubkey_auth_options = 0;
 		options->max_pubkey_ok = DEFAULT_AUTH_FAIL_MAX;
 	}
+	if (options->agent_socket_path == NULL)
+		options->agent_socket_path = xstrdup(_PATH_SSH_AGENT_SOCKET_DIR);
 
 	assemble_algorithms(options);
 
@@ -445,6 +447,7 @@ fill_default_server_options(ServerOptions *options)
 	CLEAR_ON_NONE(options->routing_domain);
 	CLEAR_ON_NONE(options->host_key_agent);
 	CLEAR_ON_NONE(options->per_source_penalty_exempt);
+	CLEAR_ON_NONE(options->agent_socket_path);
 
 	for (i = 0; i < options->num_host_key_files; i++)
 		CLEAR_ON_NONE(options->host_key_files[i]);
@@ -1673,6 +1676,29 @@ process_server_config_line_depth(ServerOptions *options, char *line,
 		intptr = &options->allow_agent_forwarding;
 		goto parse_flag;
 
+	case sAgentSocketPath:
+		charptr = &options->agent_socket_path;
+		arg = argv_next(&ac, &av);
+		if (!arg || *arg == '\0')
+			fatal("%s line %d: missing path.", filename, linenum);
+		if (strncmp(arg, "shared:", 7) == 0) {
+			/* Shared paths must be absolute */
+			if (arg[7] != '/') {
+				fatal("%s line %d: invalid shared path.",
+				    filename, linenum);
+			}
+		} else if (strncmp(arg, "user:", 5) == 0) {
+			/* User paths must not be empty */
+			if (arg[5] == '\0') {
+				fatal("%s line %d: invalid user path.",
+				    filename, linenum);
+			}
+		} else if (strcmp(arg, "none") != 0)
+			fatal("%s line %d: invalid path.", filename, linenum);
+		if (*activep && *charptr == NULL)
+			*charptr = xstrdup(arg);
+		break;
+
 	case sDisableForwarding:
 		intptr = &options->disable_forwarding;
 		goto parse_flag;
@@ -4341,6 +4367,7 @@ dump_config(ServerOptions *o)
 	dump_cfg_string(sSshdSessionPath, o->sshd_session_path);
 	dump_cfg_string(sSshdAuthPath, o->sshd_auth_path);
 	dump_cfg_string(sPerSourcePenaltyExemptList, o->per_source_penalty_exempt);
+	dump_cfg_string(sAgentSocketPath, o->agent_socket_path);
 
 	/* string arguments requiring a lookup */
 	dump_cfg_string(sLogLevel, log_level_name(o->log_level));
diff --git a/servconf.h b/servconf.h
index 089d600ad..e54e56c62 100644
--- a/servconf.h
+++ b/servconf.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.h,v 1.181 2026/09/16 00:16:52 djm Exp $ */
+/* $OpenBSD: servconf.h,v 1.182 2026/09/16 00:25:50 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo at cs.hut.fi>
@@ -240,7 +240,8 @@ SSHCONF_STRARRAY(channel_timeouts, num_channel_timeouts, ChannelTimeout, SSHCFG_
 SSHCONF_INT(unused_connection_timeout, UnusedConnectionTimeout, SSHCFG_ALL, NULL, 0, SSHCFG_COPY_MATCH) \
 SSHCONF_STRING(sshd_session_path, SshdSessionPath, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
 SSHCONF_STRING(sshd_auth_path, SshdAuthPath, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \
-SSHCONF_INTFLAG(refuse_connection, RefuseConnection, SSHCFG_ALL, 0, SSHCFG_COPY_MATCH)
+SSHCONF_INTFLAG(refuse_connection, RefuseConnection, SSHCFG_ALL, 0, SSHCFG_COPY_MATCH) \
+SSHCONF_STRING(agent_socket_path, AgentSocketPath, SSHCFG_ALL, SSHCFG_COPY_MATCH)
 
 #define SSHD_CONFIG_ENTRIES_LEGACY \
 SSHCONF_DEPRECATE(ServerKeyBits, SSHCFG_GLOBAL, SSHCONF_DEPRECATED) \
@@ -433,7 +434,6 @@ struct include_item {
 };
 TAILQ_HEAD(include_list, include_item);
 
-
 void	 initialize_server_options(ServerOptions *);
 void	 fill_default_server_options(ServerOptions *);
 int	 process_server_config_line(ServerOptions *, char *, const char *, int,
diff --git a/session.c b/session.c
index fc9c9d6f8..9125cbb83 100644
--- a/session.c
+++ b/session.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: session.c,v 1.350 2026/06/05 08:53:07 djm Exp $ */
+/* $OpenBSD: session.c,v 1.351 2026/09/16 00:25:50 djm Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -168,6 +168,7 @@ static char *auth_info_file = NULL;
 
 /* Name and directory of socket for authentication agent forwarding. */
 static char *auth_sock_name = NULL;
+static char *auth_sock_dir = NULL; /* only set if directory needs cleanup */
 
 /* removes the agent forwarding socket */
 
@@ -176,7 +177,9 @@ auth_sock_cleanup_proc(struct passwd *pw)
 {
 	if (auth_sock_name != NULL) {
 		temporarily_use_uid(pw);
-		unlink(auth_sock_name);
+		agent_listener_cleanup(options.agent_socket_path,
+		    auth_sock_name, auth_sock_dir);
+		free(auth_sock_name);
 		auth_sock_name = NULL;
 		restore_uid();
 	}
@@ -196,7 +199,9 @@ auth_input_request_forwarding(struct ssh *ssh, struct passwd *pw, int agent_new)
 	/* Temporarily drop privileged uid for mkdir/bind. */
 	temporarily_use_uid(pw);
 
-	if (agent_listener(pw->pw_dir, "sshd", &sock, &auth_sock_name) != 0) {
+	if (agent_listener(options.agent_socket_path, pw->pw_name, pw->pw_uid,
+	    pw->pw_dir, getpid(), "sshd", &sock, &auth_sock_name,
+	    &auth_sock_dir) != 0) {
 		/* a more detailed error is already logged */
 		ssh_packet_send_debug(ssh, "Agent forwarding disabled: "
 		    "couldn't create listener socket");
diff --git a/ssh-agent.1 b/ssh-agent.1
index ff1653386..bd1677e74 100644
--- a/ssh-agent.1
+++ b/ssh-agent.1
@@ -1,4 +1,4 @@
-.\" $OpenBSD: ssh-agent.1,v 1.87 2026/05/27 03:04:30 djm Exp $
+.\" $OpenBSD: ssh-agent.1,v 1.88 2026/09/16 00:25:50 djm Exp $
 .\"
 .\" Author: Tatu Ylonen <ylo at cs.hut.fi>
 .\" Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -34,7 +34,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd $Mdocdate: May 27 2026 $
+.Dd $Mdocdate: September 16 2026 $
 .Dt SSH-AGENT 1
 .Os
 .Sh NAME
@@ -43,15 +43,15 @@
 .Sh SYNOPSIS
 .Nm ssh-agent
 .Op Fl c | s
-.Op Fl \&DdTU
-.Op Fl a Ar bind_address
+.Op Fl \&DdU
+.Op Fl T | A Ar directory | Fl a Ar bind_address
 .Op Fl E Ar fingerprint_hash
 .Op Fl O Ar option
 .Op Fl P Ar allowed_providers
 .Op Fl t Ar life
 .Nm ssh-agent
-.Op Fl TU
-.Op Fl a Ar bind_address
+.Op Fl U
+.Op Fl T | A Ar directory | Fl a Ar bind_address
 .Op Fl E Ar fingerprint_hash
 .Op Fl O Ar option
 .Op Fl P Ar allowed_providers
@@ -79,8 +79,31 @@ Bind the agent to the
 .Ux Ns -domain
 socket
 .Ar bind_address .
-The default is to create a socket at a random path matching
-.Pa $HOME/.ssh/agent/s.* .
+The default is to create a socket in the
+.Pa $HOME/.ssh/agent
+directory using a random path matching
+.Pa s.* .
+.It Fl A Ar socket_path
+Specify a different directory path under which to create the socket.
+Sockets may be created in either a shared location or a user-specific
+directory.
+User-specific directories are specified by prefixing the path name with
+.Cm user: .
+.Xr ssh-agent 1
+will ensure the directory exists and create the listening socket
+directly in it.
+Relative user-specific directory paths will be created to the user's
+.Ev $HOME .
+.Pp
+Shared directories may be specified by prefixing an absolute path name with
+.Cm shared: .
+In this case, a temporary subdirectory will be created under the specified
+directory and the listening agent socket will be created in that.
+.Pp
+This option accepts the tokens described in the
+.Xr sshd_config 5
+.Sx TOKENS
+section.
 .It Fl c
 Generate C-shell commands on standard output.
 This is the default if
diff --git a/ssh-agent.c b/ssh-agent.c
index 1604f540a..2a7e1d259 100644
--- a/ssh-agent.c
+++ b/ssh-agent.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: ssh-agent.c,v 1.331 2026/08/07 05:18:05 djm Exp $ */
+/* $OpenBSD: ssh-agent.c,v 1.332 2026/09/16 00:25:50 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo at cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo at cs.hut.fi>, Espoo, Finland
@@ -63,6 +63,7 @@
 #include <time.h>
 #include <unistd.h>
 #include <util.h>
+#include <pwd.h>
 
 #include "xmalloc.h"
 #include "ssh.h"
@@ -166,7 +167,8 @@ pid_t cleanup_pid = 0;
 
 /* pathname and directory for AUTH_SOCKET */
 static char *socket_name;
-static char socket_dir[PATH_MAX];
+static char *socket_dir;
+static char *socket_dirspec;
 
 /* Pattern-list of allowed PKCS#11/Security key paths */
 static char *allowed_providers;
@@ -2190,12 +2192,12 @@ cleanup_socket(void)
 		return;
 	debug_f("cleanup");
 	if (socket_name != NULL) {
-		unlink(socket_name);
+		agent_listener_cleanup(socket_dirspec, socket_name, socket_dir);
 		free(socket_name);
 		socket_name = NULL;
+		free(socket_dir);
+		socket_dir = NULL;
 	}
-	if (socket_dir[0])
-		rmdir(socket_dir);
 }
 
 void
@@ -2238,9 +2240,11 @@ static void
 usage(void)
 {
 	fprintf(stderr,
-	    "usage: ssh-agent [-c | -s] [-DdTU] [-a bind_address] [-E fingerprint_hash]\n"
-	    "                 [-O option] [-P allowed_providers] [-t life]\n"
-	    "       ssh-agent [-TU] [-a bind_address] [-E fingerprint_hash] [-O option]\n"
+	    "usage: ssh-agent [-c | -s] [-DdU] [-T | -A directory | -a bind_address]\n"
+	    "                 [-E fingerprint_hash] [-O option]\n"
+	    "                 [-P allowed_providers] [-t life]\n"
+	    "       ssh-agent [-U] [-T | -A directory | -a bind_address]\n"
+	    "                 [-E fingerprint_hash] [-O option]\n"
 	    "                 [-P allowed_providers] [-t life] command [arg ...]\n"
 	    "       ssh-agent [-c | -s] -k\n"
 	    "       ssh-agent -u\n"
@@ -2273,6 +2277,7 @@ main(int ac, char **av)
 	u_int maxfds;
 	sigset_t nsigset, osigset;
 	int socket_activated = 0;
+	struct passwd *pw;
 
 	/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
 	sanitise_stdfd();
@@ -2283,6 +2288,10 @@ main(int ac, char **av)
 
 	platform_disable_tracing(0);	/* strict=no */
 
+	if ((pw = getpwuid(getuid())) == NULL)
+		fatal("No user exists for uid %lu", (u_long)getuid());
+	pw = pwcopy(pw);
+
 #ifdef RLIMIT_NOFILE
 	if (getrlimit(RLIMIT_NOFILE, &rlim) == -1)
 		fatal("%s: getrlimit: %s", __progname, strerror(errno));
@@ -2291,7 +2300,7 @@ main(int ac, char **av)
 	__progname = ssh_get_progname(av[0]);
 	seed_rng();
 
-	while ((ch = getopt(ac, av, "cDdksTuUVE:a:O:P:t:")) != -1) {
+	while ((ch = getopt(ac, av, "cDdksTuUVA:E:a:O:P:t:")) != -1) {
 		switch (ch) {
 		case 'E':
 			fingerprint_hash = ssh_digest_alg_by_name(optarg);
@@ -2342,6 +2351,9 @@ main(int ac, char **av)
 		case 'a':
 			agentsocket = optarg;
 			break;
+		case 'A':
+			socket_dirspec = xstrdup(optarg);
+			break;
 		case 't':
 			if ((lifetime = convtime(optarg)) == -1) {
 				fprintf(stderr, "Invalid lifetime\n");
@@ -2371,6 +2383,9 @@ main(int ac, char **av)
 	if (ac > 0 &&
 	    (c_flag || k_flag || s_flag || d_flag || D_flag || u_flag))
 		usage();
+	/* only one of -a, -A and -T allowed */
+	if (((socket_dirspec != NULL) + (agentsocket != NULL) + T_flag) > 1)
+		usage();
 
 	log_init(__progname,
 	    d_flag ? SYSLOG_LEVEL_DEBUG3 : SYSLOG_LEVEL_INFO,
@@ -2381,6 +2396,11 @@ main(int ac, char **av)
 	if (websafe_allowlist == NULL)
 		websafe_allowlist = xstrdup(DEFAULT_WEBSAFE_ALLOWLIST);
 
+	if (T_flag)
+		socket_dirspec = xstrdup(_PATH_SSH_AGENT_SOCKET_TMPDIR);
+	else if (socket_dirspec == NULL && agentsocket == NULL)
+		socket_dirspec = xstrdup(_PATH_SSH_AGENT_SOCKET_DIR);
+
 	if (ac == 0 && !c_flag && !s_flag) {
 		shell = getenv("SHELL");
 		if (shell != NULL && (len = strlen(shell)) > 2 &&
@@ -2414,7 +2434,8 @@ main(int ac, char **av)
 	if (u_flag) {
 		if ((homedir = get_homedir()) == NULL)
 			fatal("Couldn't determine home directory");
-		agent_cleanup_stale(homedir, u_flag > 1);
+		agent_cleanup_stale(socket_dirspec,
+		    pw->pw_name, pw->pw_uid, homedir, u_flag > 1);
 		printf("Deleted stale agent sockets in ~/%s\n",
 		    _PATH_SSH_AGENT_SOCKET_DIR);
 		exit(0);
@@ -2453,35 +2474,23 @@ main(int ac, char **av)
 		socket_activated = 1;
 	}
 
-	if (sock == -1 && agentsocket == NULL && !T_flag) {
-		/* Default case: ~/.ssh/agent/[socket] */
+	if (sock == -1 && agentsocket == NULL) {
+		/* Listen on a socket in/under a given directory */
 		if ((homedir = get_homedir()) == NULL)
 			fatal("Couldn't determine home directory");
-		if (!U_flag)
-			agent_cleanup_stale(homedir, 0);
-		if (agent_listener(homedir, "agent", &sock, &socket_name) != 0)
+		if (!U_flag) {
+			agent_cleanup_stale(socket_dirspec,
+			    pw->pw_name, pw->pw_uid, homedir, 0);
+		}
+		if (agent_listener(socket_dirspec, pw->pw_name, pw->pw_uid,
+		    homedir, getpid(), "local", &sock, &socket_name,
+		    &socket_dir) != 0)
 			fatal_f("Couldn't prepare agent socket");
 		free(homedir);
 	} else if (sock == -1) {
-		if (T_flag) {
-			/*
-			 * Create private directory for agent socket
-			 * in $TMPDIR.
-			 */
-			mktemp_proto(socket_dir, sizeof(socket_dir));
-			if (mkdtemp(socket_dir) == NULL) {
-				perror("mkdtemp: private socket dir");
-				exit(1);
-			}
-			xasprintf(&socket_name, "%s/agent.%ld",
-			    socket_dir, (long)parent_pid);
-		} else {
-			/* Try to use specified agent socket */
-			socket_dir[0] = '\0';
-			socket_name = xstrdup(agentsocket);
-		}
-		/* Listen on socket */
+		/* Listen on explicit socket path */
 		prev_mask = umask(0177);
+		socket_name = xstrdup(agentsocket);
 		if ((sock = unix_listener(socket_name,
 		    SSH_LISTEN_BACKLOG, 0)) < 0) {
 			*socket_name = '\0'; /* Don't unlink existing file */
@@ -2603,7 +2612,7 @@ skip:
 		fatal("%s: unveil %s %s", __progname, socket_name,
 		    strerror(errno));
 	}
-	if (*socket_dir != '\0' && unveil(socket_dir, "c") == -1) {
+	if (socket_dir != NULL && unveil(socket_dir, "c") == -1) {
 		fatal("%s: unveil %s %s", __progname, socket_dir,
 		    strerror(errno));
 	}
diff --git a/sshd_config.5 b/sshd_config.5
index 0d9f88061..bfd208901 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -33,7 +33,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.\" $OpenBSD: sshd_config.5,v 1.405 2026/09/16 00:16:52 djm Exp $
+.\" $OpenBSD: sshd_config.5,v 1.406 2026/09/16 00:25:50 djm Exp $
 .Dd $Mdocdate: September 16 2026 $
 .Dt SSHD_CONFIG 5
 .Os
@@ -97,6 +97,31 @@ Valid arguments are
 (use IPv4 only), or
 .Cm inet6
 (use IPv6 only).
+.It Cm AgentSocketPath
+Specifies the filesystem path used for forwarded
+.Xr ssh-agent 1
+sockets.
+Sockets may be created in either a shared location or a user-specific
+directory.
+User-specific directories are specified by prefixing the path name with
+.Cm user: .
+.Xr sshd 8
+will ensure the directory exists and create the listening socket
+directly in it.
+Relative user-specific directory paths will be created to the user's
+.Ev $HOME .
+.Pp
+Shared directories may be specified by prefixing an absolute path name with
+.Cm shared: .
+In this case, a temporary subdirectory will be created under the specified
+directory and the listening agent socket will be created in that.
+.Pp
+.Cm AgentSocketPath
+accepts the tokens described in the
+.Sx TOKENS
+section.
+The default path speification is
+.Pa user:.ssh/agent .
 .It Cm AllowAgentForwarding
 Specifies whether
 .Xr ssh-agent 1
@@ -2279,6 +2304,9 @@ The numeric user ID of the target user.
 The username.
 .El
 .Pp
+.Cm AgentSocketPath
+accepts the tokens %%, %h, %U, and %u.
+.Pp
 .Cm AuthorizedKeysCommand
 accepts the tokens %%, %C, %D, %f, %h, %k, %t, %U, and %u.
 .Pp

-- 
To stop receiving notification emails like this one, please contact
djm at mindrot.org.


More information about the openssh-commits mailing list