Index | Thread | Search

From:
Damien Miller <djm@mindrot.org>
Subject:
ssh: support both /tmp and ~/.ssh for agent sockets
To:
tech@openbsd.org
Cc:
openssh@openssh.com
Date:
Thu, 13 Aug 2026 13:31:47 +1000

Download raw body.

Thread
  • Damien Miller:

    ssh: support both /tmp and ~/.ssh for agent sockets

Hi,

Some time ago, we changed sshd and ssh-agent to establish $SSH_AUTH_SOCK
under ~/.ssh/agent to make unveil/pledge tighter and to reduce ambient
usage of the shared /tmp directory that they used to use.

This unfortunately causes problems in some situations, e.g. users with
no (writable) $HOME or users with $HOME on a fs that doesn't support
unix sockets. Some people at $dayjob are affected by this.

This patch gives ssh-agent and sshd more control over where it puts
their agent sockets and how it treats them, by giving it a directive
that specifies the path to where the sockets will be placed and
whether that location is a shared directory.

This allows per-user sockets like the current default, but in
arbitrary directories, e.g. `ssh-agent -A "user:/run/%U" would
place sockets in /run/[uid]/sock.XXXXXX.

`ssh-agent -A shared:/tmp` (also ssh-agent -T) would bring back
the old behaviour of creating a subdirectory for the socket,
yielding a path like /tmp/ssh-XXXXX/agent.XXXXX.

ATM just specifying a path (i.e. ssh-agent -A /foo) is an error,
but I'd like that to "do the right thing" automatically as a next
step, i.e. check the ownership and permissions of the target
directory and only use a per-user socket if they are safe.

ok?

diff --git a/misc-agent.c b/misc-agent.c
index f42a251..7742b74 100644
--- a/misc-agent.c
+++ b/misc-agent.c
@@ -28,6 +28,7 @@
 #include <string.h>
 #include <time.h>
 #include <unistd.h>
+#include <libgen.h>
 
 #include "digest.h"
 #include "log.h"
@@ -76,7 +77,7 @@ hostname_hash(size_t len)
 	return xstrdup(p);
 }
 
-char *
+static char *
 agent_hostname_hash(void)
 {
 	return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
@@ -152,71 +153,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;
 }
 
@@ -262,14 +380,25 @@ socket_is_stale(const char *path)
 }
 
 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;
+	char *prefix = NULL, *dir = NULL, *path;
 	struct timespec now, sub;
 
+	/* 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));
@@ -289,10 +418,9 @@ agent_cleanup_stale(const char *homedir, int ignore_hosthash)
 		free(path);
 	}
 
-	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;
 	}
 	while ((dp = readdir(d)) != NULL) {
@@ -301,23 +429,23 @@ 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))
 			continue;
 		if (timespeccmp(&sb.st_mtim, &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", dirpath, dp->d_name);
+		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);
@@ -327,6 +455,6 @@ agent_cleanup_stale(const char *homedir, int ignore_hosthash)
  out:
 	if (d != NULL)
 		closedir(d);
-	free(dirpath);
+	free(dir);
 	free(prefix);
 }
diff --git a/misc.c b/misc.c
index 4ce2e4e..0f24dad 100644
--- a/misc.c
+++ b/misc.c
@@ -3083,3 +3083,50 @@ get_homedir(void)
 
 	return NULL;
 }
+
+int
+mkdir_path(const char *target, mode_t mode)
+{
+	char *dir, *odir = NULL, *next;
+	int fd = AT_FDCWD, fd2, subpath_len, ret = -1;
+
+	dir = odir = xstrdup(target);
+
+	if (*dir == '/' &&
+	    (fd = open("/", O_RDONLY|O_DIRECTORY)) == -1) {
+		error_f("open(\"/\"): %s", strerror(errno));
+		return -1;
+	}
+	/* Work through the path, component-wise */
+	for (; dir != NULL && *dir != '\0'; dir = next) {
+		if ((next = strchr(dir, '/')) != NULL)
+			*(next++) = '\0';
+		if (*dir == '\0')
+			continue;
+		subpath_len = (next == NULL) ? INT_MAX : next - odir - 1;
+		if (mkdirat(fd, dir, mode) == 0)
+			debug_f("created directory %.*s", subpath_len, target);
+		else if (errno != EEXIST) {
+			error_f("mkdir(\"%.*s\"): %s",
+			    subpath_len, target, strerror(errno));
+			goto out;
+		}
+
+		/* descend */
+		if ((fd2 = openat(fd, dir, O_RDONLY|O_DIRECTORY)) == -1) {
+			error_f("open(\"%.*s\"): %s",
+			    subpath_len, target, strerror(errno));
+			goto out;
+		}
+		if (fd != AT_FDCWD)
+			close(fd);
+		fd = fd2;
+	}
+	/* success */
+	ret = 0;
+ out:
+	free(odir);
+	if (fd != AT_FDCWD)
+		close(fd);
+	return ret;
+}
diff --git a/misc.h b/misc.h
index 6ec6c3a..7ecaa56 100644
--- a/misc.h
+++ b/misc.h
@@ -113,6 +113,7 @@ int	 path_absolute(const char *);
 int	 stdfd_devnull(int, int, int);
 int	 lib_contains_symbol(const char *, const char *);
 char	*get_homedir(void);
+int	 mkdir_path(const char *, mode_t);
 
 struct passwd *pwcopy(struct passwd *);
 void	 pwfree(struct passwd *); /* NB. only use with pwcopy */
@@ -237,9 +238,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 5117def..74f527a 100644
--- a/pathnames.h
+++ b/pathnames.h
@@ -56,10 +56,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 0f9c059..900a88b 100644
--- a/servconf.c
+++ b/servconf.c
@@ -392,6 +392,8 @@ fill_default_server_options(ServerOptions *options)
 		options->sshd_session_path = xstrdup(_PATH_SSHD_SESSION);
 	if (options->sshd_auth_path == NULL)
 		options->sshd_auth_path = xstrdup(_PATH_SSHD_AUTH);
+	if (options->agent_socket_path == NULL)
+		options->agent_socket_path = xstrdup(_PATH_SSH_AGENT_SOCKET_DIR);
 
 	assemble_algorithms(options);
 
@@ -423,6 +425,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]);
@@ -1602,6 +1605,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;
@@ -4218,6 +4244,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 8ed333d..99e9183 100644
--- a/servconf.h
+++ b/servconf.h
@@ -235,7 +235,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) \
@@ -402,7 +403,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 71ab49e..3c4c748 100644
--- a/session.c
+++ b/session.c
@@ -148,6 +148,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 */
 
@@ -156,7 +157,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();
 	}
@@ -176,7 +179,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 7230003..820dd27 100644
--- a/ssh-agent.1
+++ b/ssh-agent.1
@@ -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 213f3ca..93f097a 100644
--- a/ssh-agent.c
+++ b/ssh-agent.c
@@ -60,6 +60,7 @@
 #include <time.h>
 #include <unistd.h>
 #include <util.h>
+#include <pwd.h>
 
 #include "xmalloc.h"
 #include "ssh.h"
@@ -163,7 +164,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;
@@ -2187,12 +2189,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
@@ -2235,9 +2237,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"
@@ -2265,6 +2269,7 @@ main(int ac, char **av)
 	size_t npfd = 0;
 	u_int maxfds;
 	sigset_t nsigset, osigset;
+	struct passwd *pw;
 
 	/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
 	sanitise_stdfd();
@@ -2273,10 +2278,14 @@ main(int ac, char **av)
 	(void)setegid(getgid());
 	(void)setgid(getgid());
 
+	if ((pw = getpwuid(getuid())) == NULL)
+		fatal("No user exists for uid %lu", (u_long)getuid());
+	pw = pwcopy(pw);
+
 	if (getrlimit(RLIMIT_NOFILE, &rlim) == -1)
 		fatal("%s: getrlimit: %s", __progname, strerror(errno));
 
-	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);
@@ -2327,6 +2336,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");
@@ -2356,6 +2368,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,
@@ -2366,6 +2381,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 &&
@@ -2401,7 +2421,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);
@@ -2424,35 +2445,23 @@ main(int ac, char **av)
 	 * Create socket early so it will exist before command gets run from
 	 * the parent.
 	 */
-	if (agentsocket == NULL && !T_flag) {
-		/* Default case: ~/.ssh/agent/[socket] */
+	if (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 (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 */
@@ -2552,7 +2561,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 51620b0..edc690b 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -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
@@ -2227,6 +2252,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