14

According to man sshd:

 LOGIN PROCESS
      When a user successfully logs in, sshd does the following:
  <...>

        9.   Runs user's shell or command.  All commands are run under the
             user's login shell as specified in the system password data‐
             base.

It is not clear, though, does "run under the user's login shell" literally mean "Login shell, as in bash -l"? My experiments show that no, it is not:

$ ssh u@h shopt -q login_shell && echo 'Login shell' || echo 'Not login shell'
Not login shell

I can't see why this is so? It results in commands being run doesn't get the usual environment as just logins to shell. This is cumbersome.

Anthony
  • 637

2 Answers2

15

login shell can mean two different things in that context:

  1. the shell that is defined as the login shell for the user in the account database.

    For instance:

    $ getent passwd stephane
    stephane:*:1000:1000:Stephane Chazelas:/home/stephane:/bin/zsh
    

    /bin/zsh is my login shell, the 7th field in my entry in the account database. Some other users may have /bin/tcsh, /bin/fish, /bin/bash, /sbin/nologin, /bin/false...

  2. an invocation of a shell used to initialise a login session, the ones that are usually done by login / sshd... by prefixing the argv[0] with - (some shells also accept a -l or --login option for that) and tell the shell to interpret some session initialisation files such as .profile, .login, .zlogin, etc.

In any case the sh in ssh is for shell, and sshd like rshd before it, always invokes a shell to interpret the code sent by the client if any.

If you run:

ssh user@host foo    bar

Where foo and bar arguments are passed to ssh, ssh joins them with spaces and sends the foo bar shell code to the server.

The server on host will run the login shell of user with 3 arguments:

  1. the basename of the login shell. In my case zsh
  2. -c
  3. the code given by the client: foo bar in that example

And the shell will interpret that as code in its own syntax¹, it will not be run as a login shell, the argv[0] doesn't start with a -.

If no argument is passed to the client, like when it's invoked as:

ssh user@host

Then the rlogin mode is entered (like rsh that used to run rlogin when not passed a command), then a pseudo-tty is used and sshd runs the login shell of the user with only one argument (argv[0]):

  1. the basename of the login shell prefixed with a -, in my case -zsh.

And that tells the shell it is running as a login shell and it will read .zprofile / .profile / .login.

So to sum up:

  • ssh host shell-code is like rsh host shell-code and runs a command remotely by having the login shell of the remote user parse some code.
  • ssh host is like rlogin host, and logs you in remotely by starting your login shell in login shell mode in a virtual terminal.

¹ for this very simple one, except for /bin/false or /sbin/nologin of course and maybe for users who like to have /usr/bin/python3 as their login shell, the code will interpreted the same by all shells, but for more complex ones, there will be some variation. See also How to execute an arbitrary simple command over ssh without knowing the login shell of the remote user?

  • I see... That's why I had to fix that: cd $(dirname $(which bash)); sudo ln -s bash -bash; chsh -s $(which -bash). – Anthony Apr 27 '23 at 18:00
  • If I run ssh host 'pstree -s $PPID', I see systemd---sshd---sshd---sshd---pstree, whereas if I run ssh host 'sleep 0; pstree -s $PPID', I see systemd---sshd---sshd---sshd---bash---pstree. I'm surprised that the former doesn't show bash in the tree, and also surprised that prepending sleep 0 ; causes bash to show up in the process tree. Can you explain why this happens in the context of your answer? – Daniel Kessler Jul 02 '23 at 19:54
  • 1
    @DanielKessler, yes bash optimises out the fork to execute cmd in bash -c 'cmd' but not in bash -c 'other-cmd; cmd or bash -c 'cmd > redir... Other shells like ksh or zsh optimise more aggressively and consistently. – Stéphane Chazelas Jul 02 '23 at 19:57
  • Thanks for the very quick response. If I understand correctly; in either case, sshd calls bash which calls pstree, but in the former case, due to some optimization in how the command is forked, bash doesn't appear in the pstree output. In other words, sshd is not truly directly calling pstree, right? – Daniel Kessler Jul 02 '23 at 20:00
  • 1
    Yes, sshd forks a child process that executes bash and in that same process bash, executes pstree, so during its lifetime that process has been running code from sshd, bash and pstree. Had you done ssh host 'env pstree, it would also have run env inbetween bash and pstree – Stéphane Chazelas Jul 02 '23 at 20:02
  • Your answer and comments have been enormously helpful; thank you! I've been tinkering with emacs/TRAMP/magit to try to speed up remote git operations on an HPC system where login shells (in the -l sense) are very slow (due to some cruft in /etc/bashrc), which makes using magit painful. Thanks to you, I now have a much better understanding of what I want to happen :) – Daniel Kessler Jul 02 '23 at 20:05
  • 2
    @DanielKessler, note that in ssh host cmd, sshd runs bash -c cmd, not as a login shell. Login shells don't read bashrc, just profile (though profile could source bashrc). bash -c cmd is also not an interactive shell, so bashrc should not be read either, but on some systems, bash is configured at compile time to read bashrc when invoked by sshd (interactively or not) which may be what you're seeing. – Stéphane Chazelas Jul 02 '23 at 20:12
  • Thanks! I think my issue is actually that, AFAICT, when TRAMP starts up a new process, it first does exec ssh hostname to get the process started (which will invoke a login shell that will fire bashrc). As a result, magit commands that use the existing TRAMP process (e.g., staging an entire file) are very fast on my remote (since they send the command to the already-running remote shell), but those that trigger a new TRAMP process (e.g., staging a hunk within a file) are slow (because they start a new ssh connection, wait for bashrc to run, and then send the command). – Daniel Kessler Jul 02 '23 at 20:20
  • 1
    @DanielKessler, again, login shells in bash don't read bashrc unless the .profile sources it (which they often do to work around that misdesign of bash). It's not only bash reading .profile or .bashrc that's slow when you do ssh host or ssh host cmd, establishing the ssh connection and authentication can be slow. ssh can be told to reuse a previous connection. See ControlMaster, ControlPersist in man ssh_config. – Stéphane Chazelas Jul 02 '23 at 20:25
  • This useless separation of .bashrc/.bash_profile should be just eliminated. To do so, I recommend to stick with "just" .profile - as a side effect it forces you to write portable shell scripts in the first place. Personally my original problem wast that ssh doesn't execute "login shell" init files when executes commands instead of shell. – Anthony Aug 07 '23 at 22:07
14

No, SSH only invokes a login shell when logging you into a shell, not when running a command. This means that if you want your login initialization files to run (e.g. ~/.profile), you need to do this explicitly. This is a deliberate design choice: in the days where most logins were on a text terminal, many users would run commands in their .profile which only make sense when logging in for an interactive session. (This is much less common now that most people log in locally to a graphical session.) You wouldn't want a quick ssh example.com ls, or worse an rsync example.com:somefile ., to start Emacs and turn on your modem to fetch your email!


The command in SSH is a string, obtained by concatenating the command line arguments with a space in between. For example ssh u@h shopt -q login_shell runs the command shopt -q login_shell. It's exactly equivalent to ssh u@h 'shopt -q login_shell', or ssh u@h 'shopt -q' login_shell, etc.

SSH passes that string to the user's login shell. SSH does not know about any other shell. The server might be running on a non-Unix system or in a restricted environment where there is no program called sh or bash.

The SSH server runs the executable that is listed in the user database as the user's login shell, but not necessarily as a login shell. When the command line is non-empty, it passes a three-argument list:

  • argv[0] is the program's base name, following the usual convention.
  • argv[1] is the string -c.
  • argv[2] is the command passed by the client.

For an empty command line, the SSH server invokes the user's login shell as a login shell. This means a one-argument list:

  • argv[0] is a dash (-) followed by the program's base name.

For example, here's the output of ssh localhost 'cat /proc/$$/cmdline | tr \\0 \\n' on a Linux machine:

sh
-c
cat /proc/$$/cmdline | tr \\0 \\n

For a login shell, it's more difficult to observe, because the login shell itself will typically run other programs. The easy way to see what's going on is to temporarily disable the login shell's initialization file(s), for example by temporarily renaming ~/.profile (or ~/.bash_profile, ~/.zprofile, etc. depending on which shell you use). Here's an example on an account with /bin/sh as the login shell:

$ mv ~/.profile ~/not.profile
$ ssh localhost
(/etc/motd content goes here)
Last login: Mon Apr 24 18:00:30 2023
$ cat /proc/$$/cmdline; echo
-sh
$ exit
Connection to localhost closed.
$ mv ~/not.profile ~/.profile
terdon
  • 242,166