Feature request/EOI: Match interactive config?

Damien Miller
Mon, 06 May 2024 03:18:01 -0000

On Sat, 4 May 2024, openssh at tr.id.au wrote:

> Hey there,
>
> I often want different behavior in my ssh client depending on
> whether I'm logging into an interactive session or running
> a remote non-interactive command. We can see at, say,
> https://unix.stackexchange.com/a/499562/305714 that this isn't a
> unique wish, and existing solutions are kind of baroque. Typical
> reasons to do this are to immediately go into a screen or tmux
> session; for myself, I often want to relaunch bash as "bash -lo
> vi" on boxes where I don't have bashrc control. Basically, we want
> RemoteCommand to be turned on for interactive sessions, but ignore
> it when we've already specified a command as part of the client
> invocation.
>
> I wondered if there would be support for, or interest in, adding a
> new condition called "interactive" (or similar) to the Match
keyword?
> Although my use case is for client-side, I guess it may also make
> sense in sshd_config. I can imagine cases where sysadmins would want
> to present different behavior depending on whether a client is coming
> in interactively or running a command.
>
> Alternatively, could there be a new option which specifies how to
> resolve conflicts between command-line commands and RemoteCommand
> directives? Eg something like "RemoteCommandOptional yes" which
can be
> paired with RemoteCommand. This would allow a default RemoteCommand
> which can be overridden by commands passed on cli.
>
> Or have I overlooked an already-existing simpler/better way of
> toggling different configurations for interactive vs non-interactive
> sessions exist, when serverside control is not an option? Sorry if
> this was already discussed before, nothing from this mailing list
> turned up in a web search about the topic.

Would something like this help?

Match sessiontype shell
	User foo
Match remotecommand "none"
	User foo2
Match sessiontype exec remotecommand "*/rsync*"
	User bar
Match sessiontype subsystem remotecommand "sftp"
	User baz


diff --git a/readconf.c b/readconf.c
index 3a64a0441..dff6a9df6 100644
--- a/readconf.c
+++ b/readconf.c
@@ -70,6 +70,7 @@
 #include "uidswap.h"
 #include "myproposal.h"
 #include "digest.h"
+#include "sshbuf.h"

 /* Format of the configuration file:

@@ -133,11 +134,11 @@
 */

 static int read_config_file_depth(const char *filename, struct passwd *pw,
-    const char *host, const char *original_host, Options *options,
-    int flags, int *activep, int *want_final_pass, int depth);
+    const char *host, const char *original_host, struct sshbuf *remote_command,
+    Options *options, int flags, int *activep, int *want_final_pass, int
depth);
 static int process_config_line_depth(Options *options, struct passwd *pw,
-    const char *host, const char *original_host, char *line,
-    const char *filename, int linenum, int *activep, int flags,
+    const char *host, const char *original_host, struct sshbuf *remote_command,
+    char *line, const char *filename, int linenum, int *activep, int flags,
     int *want_final_pass, int depth);

 /* Keyword tokens. */
@@ -650,8 +651,9 @@ check_match_ifaddrs(const char *addrlist)
  */
 static int
 match_cfg_line(Options *options, char **condition, struct passwd *pw,
-    const char *host_arg, const char *original_host, int final_pass,
-    int *want_final_pass, const char *filename, int linenum)
+    const char *host_arg, const char *original_host,
+    struct sshbuf *rcommand, int final_pass, int *want_final_pass,
+    const char *filename, int linenum)
 {
 	char *arg, *oattrib, *attrib, *cmd, *cp = *condition, *host, *criteria;
 	const char *ruser;
@@ -764,6 +766,30 @@ match_cfg_line(Options *options, char **condition, struct
passwd *pw,
 			r = match_pattern_list(criteria, arg, 0) == 1;
 			if (r == (negate ? 1 : 0))
 				this_result = result = 0;
+		} else if (strcasecmp(attrib, "sessiontype") == 0) {
+			if (options->session_type == SESSION_TYPE_SUBSYSTEM)
+				criteria = xstrdup("subsystem");
+			else if (options->session_type == SESSION_TYPE_NONE)
+				criteria = xstrdup("none");
+			else if (rcommand != NULL && sshbuf_len(rcommand) > 0)
+				criteria = xstrdup("exec");
+			else
+				criteria = xstrdup("shell");
+			r = match_pattern_list(criteria, arg, 0) == 1;
+			if (r == (negate ? 1 : 0))
+				this_result = result = 0;
+		} else if (strcasecmp(attrib, "remotecommand") == 0) {
+			if (rcommand != NULL && sshbuf_len(rcommand) > 0) {
+				if ((criteria +				    sshbuf_dup_string(rcommand)) == NULL)
+					fatal_f("dup command failed");
+			} else if (options->remote_command != NULL)
+				criteria = xstrdup(options->remote_command);
+			else
+				criteria = xstrdup("none");
+			r = match_pattern_list(criteria, arg, 0) == 1;
+			if (r == (negate ? 1 : 0))
+				this_result = result = 0;
 		} else if (strcasecmp(attrib, "exec") == 0) {
 			char *conn_hash_hex, *keyalias, *jmphost;

@@ -1031,18 +1057,19 @@ parse_multistate_value(const char *arg, const char
*filename, int linenum,
  */
 int
 process_config_line(Options *options, struct passwd *pw, const char *host,
-    const char *original_host, char *line, const char *filename,
-    int linenum, int *activep, int flags)
+    const char *original_host, struct sshbuf *remote_command,
+    char *line, const char *filename, int linenum, int *activep, int flags)
 {
 	return process_config_line_depth(options, pw, host, original_host,
-	    line, filename, linenum, activep, flags, NULL, 0);
+	    remote_command, line, filename, linenum, activep, flags, NULL, 0);
 }

 #define WHITESPACE " \t\r\n"
 static int
 process_config_line_depth(Options *options, struct passwd *pw, const char
*host,
-    const char *original_host, char *line, const char *filename,
-    int linenum, int *activep, int flags, int *want_final_pass, int depth)
+    const char *original_host, struct sshbuf *remote_command, char *line,
+    const char *filename, int linenum, int *activep, int flags,
+    int *want_final_pass, int depth)
 {
 	char *str, **charptr, *endofnumber, *keyword, *arg, *arg2, *p;
 	char **cpptr, ***cppptr, fwdarg[256];
@@ -1779,7 +1806,7 @@ parse_pubkey_algos:
 			goto out;
 		}
 		value = match_cfg_line(options, &str, pw, host, original_host,
-		    flags & SSHCONF_FINAL, want_final_pass,
+		    remote_command, flags & SSHCONF_FINAL, want_final_pass,
 		    filename, linenum);
 		if (value < 0) {
 			error("%.200s line %d: Bad Match condition", filename,
@@ -2028,8 +2055,8 @@ parse_pubkey_algos:
 				    gl.gl_pathv[i], depth,
 				    oactive ? "" : " (parse only)");
 				r = read_config_file_depth(gl.gl_pathv[i],
-				    pw, host, original_host, options,
-				    flags | SSHCONF_CHECKPERM |
+				    pw, host, original_host, remote_command,
+				    options, flags | SSHCONF_CHECKPERM |
 				    (oactive ? 0 : SSHCONF_NEVERMATCH),
 				    activep, want_final_pass, depth + 1);
 				if (r != 1 && errno != ENOENT) {
@@ -2429,20 +2456,20 @@ parse_pubkey_algos:
  */
 int
 read_config_file(const char *filename, struct passwd *pw, const char *host,
-    const char *original_host, Options *options, int flags,
-    int *want_final_pass)
+    const char *original_host, struct sshbuf *remote_command,
+    Options *options, int flags, int *want_final_pass)
 {
 	int active = 1;

 	return read_config_file_depth(filename, pw, host, original_host,
-	    options, flags, &active, want_final_pass, 0);
+	    remote_command, options, flags, &active, want_final_pass, 0);
 }

 #define READCONF_MAX_DEPTH	16
 static int
 read_config_file_depth(const char *filename, struct passwd *pw,
-    const char *host, const char *original_host, Options *options,
-    int flags, int *activep, int *want_final_pass, int depth)
+    const char *host, const char *original_host, struct sshbuf *remote_command,
+    Options *options, int flags, int *activep, int *want_final_pass, int depth)
 {
 	FILE *f;
 	char *line = NULL;
@@ -2482,8 +2509,8 @@ read_config_file_depth(const char *filename, struct passwd
*pw,
 		 * line numbers later for error messages.
 		 */
 		if (process_config_line_depth(options, pw, host, original_host,
-		    line, filename, linenum, activep, flags, want_final_pass,
-		    depth) != 0)
+		    remote_command, line, filename, linenum, activep, flags,
+		    want_final_pass, depth) != 0)
 			bad_options++;
 	}
 	free(line);
diff --git a/readconf.h b/readconf.h
index 9447d5d6e..3da495e38 100644
--- a/readconf.h
+++ b/readconf.h
@@ -231,6 +231,8 @@ typedef struct {
 #define SSH_KEYSTROKE_CHAFF_MIN_MS		1024
 #define SSH_KEYSTROKE_CHAFF_RNG_MS		2048

+struct sshbuf;
+
 const char *kex_default_pk_alg(void);
 char	*ssh_connection_hash(const char *thishost, const char *host,
     const char *portstr, const char *user, const char *jump_host);
@@ -239,9 +241,9 @@ int      fill_default_options(Options *);
 void	 fill_default_options_for_canonicalization(Options *);
 void	 free_options(Options *o);
 int	 process_config_line(Options *, struct passwd *, const char *,
-    const char *, char *, const char *, int, int *, int);
+    const char *, struct sshbuf *, char *, const char *, int, int *, int);
 int	 read_config_file(const char *, struct passwd *, const char *,
-    const char *, Options *, int, int *);
+    const char *, struct sshbuf *, Options *, int, int *);
 int	 parse_forward(struct Forward *, const char *, int, int);
 int	 parse_jump(const char *, Options *, int);
 int	 parse_ssh_uri(const char *, char **, char **, int *);
diff --git a/ssh-keysign.c b/ssh-keysign.c
index 968344e79..4913b540d 100644
--- a/ssh-keysign.c
+++ b/ssh-keysign.c
@@ -222,7 +222,7 @@ main(int argc, char **argv)

 	/* verify that ssh-keysign is enabled by the admin */
 	initialize_options(&options);
-	(void)read_config_file(_PATH_HOST_CONFIG_FILE, pw, "", "",
+	(void)read_config_file(_PATH_HOST_CONFIG_FILE, pw, "", "",
NULL,
 	    &options, 0, NULL);
 	(void)fill_default_options(&options);
 	if (options.enable_ssh_keysign != 1)
diff --git a/ssh.c b/ssh.c
index 0019281f4..ecaff3844 100644
--- a/ssh.c
+++ b/ssh.c
@@ -566,7 +566,8 @@ process_config_files(const char *host_name, struct passwd
*pw, int final_pass,

 	if (config != NULL) {
 		if (strcasecmp(config, "none") != 0 &&
-		    !read_config_file(config, pw, host, host_name, &options,
+		    !read_config_file(config, pw, host, host_name,
+		    command, &options,
 		    SSHCONF_USERCONF | (final_pass ? SSHCONF_FINAL : 0),
 		    want_final_pass))
 			fatal("Can't open user config file %.100s: "
@@ -576,12 +577,13 @@ process_config_files(const char *host_name, struct passwd
*pw, int final_pass,
 		    _PATH_SSH_USER_CONFFILE);
 		if (r > 0 && (size_t)r < sizeof(buf))
 			(void)read_config_file(buf, pw, host, host_name,
-			    &options, SSHCONF_CHECKPERM | SSHCONF_USERCONF |
+			    command, &options,
+			    SSHCONF_CHECKPERM | SSHCONF_USERCONF |
 			    (final_pass ? SSHCONF_FINAL : 0), want_final_pass);

 		/* Read systemwide configuration file after user config. */
 		(void)read_config_file(_PATH_HOST_CONFIG_FILE, pw,
-		    host, host_name, &options,
+		    host, host_name, command, &options,
 		    final_pass ? SSHCONF_FINAL : 0, want_final_pass);
 	}
 }
@@ -1074,7 +1076,7 @@ main(int ac, char **av)
 		case 'o':
 			line = xstrdup(optarg);
 			if (process_config_line(&options, pw,
-			    host ? host : "", host ? host : "", line,
+			    host ? host : "", host ? host : "", NULL, line,
 			    "command-line", 0, NULL, SSHCONF_USERCONF) != 0)
 				exit(255);
 			free(line);
diff --git a/ssh_config.5 b/ssh_config.5
index 2931d807e..0500bc049 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -145,6 +145,8 @@ The available criteria keywords are:
 .Cm host ,
 .Cm originalhost ,
 .Cm tagged ,
+.Cm sessiontype ,
+.Cm remotecommand ,
 .Cm user ,
 and
 .Cm localuser .
@@ -212,6 +214,7 @@ The other keywords' criteria must be single entries or
comma-separated
 lists and may use the wildcard and negation operators described in the
 .Sx PATTERNS
 section.
+.Pp
 The criteria for the
 .Cm host
 keyword are matched against the target hostname, after any substitution
@@ -223,6 +226,7 @@ options.
 The
 .Cm originalhost
 keyword matches against the hostname as it was specified on the command-line.
+.Pp
 The
 .Cm tagged
 keyword matches a tag name specified by a prior
@@ -232,6 +236,7 @@ directive or on the
 command-line using the
 .Fl P
 flag.
+.Pp
 The
 .Cm user
 keyword matches against the target username on the remote host.
@@ -242,6 +247,31 @@ keyword matches against the name of the local user running
 (this keyword may be useful in system-wide
 .Nm
 files).
+.Pp
+The
+.Cm sessiontype
+keyword matches the requested remote session type as a pattern-list, that may
+contain any of
+.Cm shell ,
+.Cm exec
+(for command execution),
+.Cm subsystem
+(e.g. for
+.Xr sftp 1
+sessions),
+or
+.Xr none
+for empty sessions, such as when
+.Xr ssh 1
+is started with the
+.Fl N
+flag.
+The
+.Cm remotecommand
+keyword matches against the remote command as a pattern-list.
+For example
+.Dq shutdown,reboot,halt*
+would match any of these commands.
 .It Cm AddKeysToAgent
 Specifies whether keys should be automatically added to a running
 .Xr ssh-agent 1 .