From: Damien Miller 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 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 #include #include +#include #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 #include #include +#include #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