7

There was a vote to close this as "unclear what you're asking". Apparently being thorough isn't a good thing, so I'll be terse:

When you SSH to a remote host: is it possible to execute a program directly instead of being executed under the user's default shell.

  • the network is air gapped
  • everyone on the network has shell access to every machine
  • IT can make whatever changes would be needed, provided those changes wouldn't increase the risk of someone getting a root shell (beyond what's already possible anyway).
  • As proof that something like it is possible: sftp, an ssh "subsystem", is able to execute without running the user's default shell, whereas the scp subsystem still uses it.
    • I tried implementing a subsystem, but it still executed under the user's default shell.

I don't think there is a good solution to this problem (short of convincing a large number of non-*nix people to abandon the crutch they've been leaning on for years), but I thought I'd put the question out there in hopes I'm mistaken.


Below is the less terse question.

In my office there's a large number of people (3-500+) all using a variety of strange and bizarre configurations. It's very hard to predict how the software we write will behave for a given user since their environment could totally violate our expectations.

To workaround this I changed our build to strip the user's environment so it couldn't affect the build, and based on the success of that strategy we further wrapped our software's execution in a shell script that does the same sort of thing.

Unfortunately, the launcher can still be affected by the user's environment. When it is executed remotely, the user's .cshrc/.login typically have a bunch of junk that causes long delays before the launcher even begins executing.

Normally I'd be inclined to say "fix your crap", but we're not talking about *nix-savvy people; this is a crutch they've leaned on for years and they're more inclined to continue to blame our software for their configuration issues.

In short, I'm looking for a way to completely short-circuit their goofy environment scripts.

To simulate this situation I set my default shell to tcsh and put hostname; sleep 10 in my .cshrc, then ran a few tests:

  • sftp does not print/sleep -- though, the target machine in that case was solaris where sftp is built into sshd, and I didn't think to test against a linux box. Running ptree against the process tree servicing the sftp connection shows it's not running under a shell.
  • scp prints the text & sleeps for 10s
  • every variant of executing commands on the remote host through ssh causes the command to run under the user's default shell; I tried:
    • The obvious -- pass a command in over ssh
    • command= in .ssh/authorized_keys
    • I made a custom "subsystem" in my personal linux /etc/ssh/sshd_config
  • Enabled PermitUserEnvironment on my machine to try setting SHELL
    • Didn't work at all: SHELL=/bin/bash ssh -o "SendEnv SHELL" localhost
    • Kinda worked: Use environment= or ~/.ssh/enviornment to set SHELL
      • It kinda worked in that SHELL was set, but it actually executes under my true default shell

I managed to use netcat to forward a shell over an existing ssh session, but that seems like using a sledge to crack an egg -- both messy and highly inappropriate.


One person in our organization suggested making another user that, through whatever means, everyone would be able to login as that user over ssh, then ::cloudy bubble solution with a lot of hand waving:: to get the program running as the original user.

I'm a little dubious about doing something like that. It may not be as bad as forwarding a shell with netcat, or something heavy-handed and brittle like temporarily renaming their .cshrc, but I'm still not fond of it.

Any suggestions are welcome.

9
  • Okay, so your software is invoked by running a shell script that strips the user's environment, and that works if that shell script is invoked locally, but it doesn't work if someone first SSH's into another machine and invokes the script there? Is that what you're saying?
    – Spiff
    Mar 17, 2015 at 23:54
  • I'll edit to word it better. It still behaves properly, but if their .cshrc does a bunch of crap first (hence the sleep 10) it can create long delays. Mar 18, 2015 at 0:01
  • Does the problematic user environment have anything in ~/.ssh/rc or /etc/ssh/sshrc?
    – Spiff
    Mar 18, 2015 at 0:04
  • Nope. I've boiled it down to the simplest case with the example I gave: set your default shell to tcsh, put sleep 10 in your .cshrc, and try to ssh into that machine and run a command without causing .cshrc contents to be executed. Mar 18, 2015 at 0:06
  • Is running an alternate shell at some point in the process possible? Something like ssh -t <host> sh? Or even something like ssh <host> csh -f [<command>]? (The docs I read says -f disables reading startup files.)
    – snapshoe
    Mar 22, 2015 at 18:30

4 Answers 4

2

It should be possible to accomplish what you desire with a subsystem, but I am fairly certain it will require writing a custom sshd binary, as the OpenSSH code only provides the sftp subsystem, and merely sets up an extendable framework for additional definitions.

1
  • One can define a custom subsystem by adding Subsystem … entry in sshd_config. Unfortunately the supplied command will be executed in the user's shell, not directly, so this is not a solution. I'm afraid you're right about the requirement to write a custom sshd binary. Feb 8, 2022 at 11:41
2

At least with Bash, ssh with the "-t" argument (e.g. ssh server1 -t '/foo/bar/script.sh') will run a remote command without executing "~/.bashrc" (the bash equivalent to "~/.cshrc/login").

2
  • That was one of the more irritating aspects of this problem. If the user's default shell is bash it wasn't difficult at all to avoid having .bashrc get sourced. csh and tcsh, on the other hand, are a little more irritating. Apr 16, 2015 at 21:03
  • I dunno why I didn't upvote this question. While it didn't satisfy my needs, it's still potentially valuable to someone else. Jul 21, 2015 at 15:37
1

Create a custom config file in /etc/ssh/sshd_config.d with this content:

Match User yourUserName
SetEnv SHELL=/usr/bin/bash # Or whatever other shell you're comfortable with
1
  • Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
    – Community Bot
    Feb 8, 2022 at 9:31
0

Idea

The following solution is not as elegant as you may wish, but it works.

  • On the server, for each shell used as some user's default shell and for each shell that may be used as such, i.e. for each shell in /etc/shells, create a wrapper.

  • Each wrapper should be written in a language that doesn't process goofy files by default.

  • Each wrapper should do this:

    1. Check if SSH_OVERRIDE_SHELL is a non-empty variable.

    2. If SSH_OVERRIDE_SHELL is non-empty, the wrapper should exec to a clean shell that doesn't process goofy files. In a POSIX shell this may look like this:

      exec env -u SSH_OVERRIDE_SHELL /path/to/real/bash --norc --noprofile "$@"
      

      Note ultimately the real bash will replace the wrapper because env itself execs (at least it does so in my Debian). So if the wrapper is invoked by sshd then ultimately it will be as if bash was invoked by sshd. And this bash will receive all the arguments the wrapper gets, plus --norc and --noprofile.

    3. If SSH_OVERRIDE_SHELL is unset or empty, the wrapper should exec to the real default shell of the user. In a POSIX shell this may look like this:

      exec env -u SSH_OVERRIDE_SHELL /path/to/real/relevant_shell "$@"
      

    I used env to unset SSH_OVERRIDE_SHELL, but you can use whatever the language of the wrapper provides to modify the environment beforehand.

  • Then you need to allow SSH clients to push SSH_OVERRIDE_SHELL to the server. You need AcceptEnv SSH_OVERRIDE_SHELL in sshd_config on the server. Remember for each keyword (like AcceptEnv) the first obtained value will be used, so if an AcceptEnv line already exists then you need to add SSH_OVERRIDE_SHELL to it rather than adding a separate line. Reload the SSH server.

  • A client whose default shell on the server is really one of our wrappers can now run some shell code remotely in a clean bash like this:

    # client-side
    SSH_OVERRIDE_SHELL=whatever ssh -o SendEnv=SSH_OVERRIDE_SHELL user@server 'shell code here'
    

    Any usage of ssh user@server that doesn't send non-empty SSH_OVERRIDE_SHELL to the server will work as before. Similarly any usage of a wrapper on the server without having a non-empty SSH_OVERRIDE_SHELL in the environment will work as if the wrapper was the real shell it replaces.


Potential problem

Replacing /bin/bash, /usr/bin/zsh etc. with wrappers and moving real shells to elsewhere may interfere with upgrading the server in the future. Upgrades (like apt-get upgrade) that overwrite these files will overwrite the wrappers. I don't know update-alternatives of Debian good enough to tell if it can solve this problem (at least in Debian and derivatives). In general consider a slightly different approach.


Slightly different approach

The slightly different approach is to leave all shells where they should be and to place wrappers in a dedicated directory (or directory tree). Every entry in /etc/shells should be updated and point to the respective wrapper. For every user you want to implement this solution for, the default shell (like in /etc/passwd) should be updated and point to the respective wrapper. After this the affected users will use wrappers and (at least regular users without sudo access) will be able to only choose a wrapper as their login shell (with chsh).

Note this method allows you (or rather the IT department) to leave some users unaffected for any reason. I think especially root shall be left alone, but also at least one sudoer. If there is something wrong with the wrappers, you want at least one admin to be able to log in.

Affected users may ask why formally their login shells and the output of echo "$SHELL" have changed; still in practice they shouldn't experience any difference. Note each wrapper may "fix" the SHELL variable and make it look as if the wrapper wasn't there.


Proof of concept

This is what I did on my server (as root):

# prepare directory structure
mkdir -p /shell_wrappers/bin
mkdir -p /shell_wrappers/usr/bin

# prepare symlinks
ln -s /shell_wrappers/wrapper /shell_wrappers/bin/bash
ln -s /shell_wrappers/wrapper /shell_wrappers/usr/bin/zsh

# create an universal wrapper
>/shell_wrappers/wrapper cat <<'EOF'
#!/bin/sh
SHELL="${SHELL#/shell_wrappers}"
if [ -n "$SSH_OVERRIDE_SHELL" ]; then
   unset SSH_OVERRIDE_SHELL
   exec /bin/bash --norc --noprofile "$@"
else
   unset SSH_OVERRIDE_SHELL
   exec "$SHELL" "$@"
fi
EOF

# make the wrapper executable
chmod +x /shell_wrappers/wrapper

The wrapper is universal. To wrap a shell whose path is /bar/baz/shell, you need a symlink at /shell_wrappers/bar/baz/shell. The symlink must point to /shell_wrappers/wrapper.

Next I modified /etc/ssh/sshd_config and added SSH_OVERRIDE_SHELL to AcceptEnv. There were other tokens already, so in my case the resulting line was:

AcceptEnv LANG LC_* SSH_OVERRIDE_SHELL

I saved the file and reloaded the SSH server service.

Then I used vipw to modify my regular user's entry. I changed /bin/bash to /shell_wrappers/bin/bash. In some of my tests I used /shell_wrappers/usr/bin/zsh.

And yes, this works. I deliberately injected commands like echo foo in my startup scripts on the server. A local command like ssh kamil@server : did print foo; but this:

SSH_OVERRIDE_SHELL=1 ssh -o SendEnv=SSH_OVERRIDE_SHELL kamil@server :

did not.

Then with /shell_wrappers/usr/bin/zsh as my login shell at the server, I did this locally:

ssh kamil@server 'ls -l /proc/$$/exe;:'

and it revealed the shell is zsh (after printing foo); but this:

SSH_OVERRIDE_SHELL=1 ssh -o SendEnv=SSH_OVERRIDE_SHELL kamil@server 'ls -l /proc/$$/exe;:'

revealed bash (without printing foo). It was this bash the wrapper runs with --norc and --noprofile.

Additionally I tried to scp a file (using SCP, not SFTP; see this answer). Noisy startup scripts (i.e. this foo being printed) broke it. Our solution worked:

SSH_OVERRIDE_SHELL=1 scp -o SendEnv=SSH_OVERRIDE_SHELL kamil@server:file1 ./

Possible improvements

  • In the universal wrapper I used unset to unset SSH_OVERRIDE_SHELL. As stated before, you can use exec env … for this. env -i allows you to use an empty environment if this is what you want.

  • Before exec /bin/bash --norc --noprofile "$@" you can cd to some desired location. Or you can run some additional code (e.g. logger "SSH_OVERRIDE_SHELL used. $SSH_CONNECTION"). You are in control.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .