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

Damien Miller <[email protected]>
Newsgroups gmane.os.openbsd.tech
Message-ID <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.