2019-06-19
How Bash decides it's being invoked through sshd and sources your .bashrc
Under normal circumstances, Bash only sources your .bashrc
when
it's run as an interactive non-login shell; for example, this is
what the Bash manual says about startup files.
Well, it is most of what the manual says, because there is an
important exception, which the Bash manual describes as 'Invoked
by remote shell daemon':
Bash attempts to determine when it is being run with its standard input connected to a network connection, as when executed by the remote shell daemon, usually rshd, or the secure shell daemon sshd. If Bash determines it is being run in this fashion, it reads and executes commands from
~/.bashrc
, [...]
(You can tell how old this paragraph of the manual is because of how much prominence it gives to rshd. Also, note that this specific phrasing about standard input presages my discovery of when bash doesn't do this.)
As the result of recent events, I became
interested in discovering exactly how Bash decides that it's being
run in the form of 'ssh host command
' and sources your .bashrc
.
There turn out to be two parts to this answer, but the summary is
that if this is enabled at all, Bash will always source your
.bashrc
for non-interactive commands if you've logged in to a
machine via SSH.
First, this feature may not even be enabled in your version of Bash, because it's a non-default configuration setting (and has been since Bash 2.05a, which is pretty old). Debian and thus Ubuntu turn this feature on, as does Fedora, but the FreeBSD machine I have access to doesn't in the version of Bash that's in its ports. Unsurprisingly, OmniOS doesn't seem to either. If you compile Bash yourself without manually changing the relevant bit of config-top.h, you'll get a version without this.
(Based on some digging, I think that Arch Linux also builds Bash without enabling this, since they don't seem to patch config-top.h. I will leave it to energetic people to check other Linuxes and other *BSDs.)
Second, how it works is actually very simple. In practice, a
non-interactive Bash decides that it is being invoked by SSHD if
either $SSH_CLIENT
or $SSH2_CLIENT
are defined in the
environment. In a robotic sense this is perfectly
correct, since OpenSSH's sshd
puts $SSH_CLIENT
in the environment
when you do 'ssh host command
'. In practice it is wrong, because
OpenSSH sets $SSH_CLIENT
all the time, including for logins.
So if you use SSH to log in somewhere, $SSH_CLIENT
will be set
in your shell environment, and then any non-interactive Bash will
decide that it should source ~/.bashrc
. This includes, for
example,
the Bash that is run (as 'bash -c ...
') to execute commands when
you have a Makefile that has explicitly set 'SHELL=/bin/bash
', as
Makefiles that are created by the GNU autoconfigure system tend to
do.
As a result, if you have ancient historical things
in a .bashrc
, for example clearing the screen on exit, then
surprise, those things will happen for every command that make
runs. This may not make you happy. For situations like Makefiles
that explicitly set 'SHELL=/bin/bash
', this can happen even if
you don't use Bash as your login shell and haven't had anything to
do with it for years.
(Of course it also happens if you have perfectly modern things there
and expect that they won't get invoked for non-interactive shells,
and you do use Bash as your login shell. But if you use Bash as
your login shell, you're more likely to notice this issue, because
routine ordinary activities like 'ssh host command
' or 'rsync
host:/something .
' are more likely to fail, or at least do additional
odd things.)
PS: This October 2001 comment in variables.c
sort
of suggests why support for this feature is now an opt-in thing.
PPS: If you want to see if your version of Bash has this enabled, the
simple way to tell is to run strings
on the binary and see if the
embedded strings include 'SSH_CLIENT
'. Eg:
; /etc/fedora-release Fedora release 29 (Twenty Nine) ; strings -a /usr/bin/bash | fgrep SSH_CLIENT SSH_CLIENT
So the Fedora 29 version does have this somewhat dubious feature enabled. Perhaps Debian and Fedora feel stuck with it due to very long-going backwards compatibility, where people would be upset if Bash stopped doing this in some new Debian or Fedora release.
Sidebar: The actual code involved
The code for this can currently be found in run_startup_files
in shell.c
:
/* get the rshd/sshd case out of the way first. */ if (interactive_shell == 0 && no_rc == 0 && login_shell == 0 && act_like_sh == 0 && command_execution_string) { #ifdef SSH_SOURCE_BASHRC run_by_ssh = (find_variable ("SSH_CLIENT") != (SHELL_VAR *)0) || (find_variable ("SSH2_CLIENT") != (SHELL_VAR *)0); #else run_by_ssh = 0; #endif [...]
Here we can see that the current Bash source code is entirely aware that no one uses rshd any more, among other things.
A Let's Encrypt client feature I always want for easy standard deployment
On Twitter, I said:
It bums me out that Certbot (the 'official' Let's Encrypt client) does not have a built-in option to combine trying a standalone HTTP server with a webroot if the standalone HTTP server can't start.
(As far as I can see.)
For Let's Encrypt authentication, 'try a standalone server, then fall back to webroot' lets you create a single setup that works in a huge number of cases, including on initial installs before Apache/etc has its certificates and is running.
In straightforward setups, the easiest way to prove your control of a domain to Let's Encrypt is generally their HTTP authentication method, which requires the host (or something standing in for it) to serve a file under a specific URL. To do this, you need a suitably configured web server.
Like most Let's Encrypt clients, Certbot supports both putting the magic files for Let's Encrypt in a directory of your choice (which is assumed to already be configured in some web server you're running) or temporarily running its own little web server to do this itself. But it doesn't support trying both at once, and this leaves you with a problem if you want to deploy a single standard Certbot configuration to all of your machines, some of which run a web server and some of which don't. And on machines that do run a web server, it is not necessarily running when you get the initial TLS certificates, because at least some web servers refuse to start at all if you have HTTPS configured and the TLS certificates are missing (because, you know, you haven't gotten them yet).
Acmetool, my favorite Let's
Encrypt client, supports exactly this dual-mode operation and it
is marvelously convenient. You can run one acmetool
command no
matter how the system is configured, and it works. If acmetool
can bind to port 80, it runs its own web server; if it can't, it
assumes that your webroot setting is good. But, unfortunately,
we need a new Let's Encrypt client.
For Certbot, I can imagine a complicated scheme of additional software and pre-request and post-request hooks to make this work; you could start a little static file only web server if there wasn't already something on port 80, then stop it afterward. But that requires additional software and is annoyingly complicated (and I can imagine failure modes). For extra annoyance, it appears that Certbot does not have convenient commands to change the authentication mode associated configured for any particular certificate (which will be used when certbot auto-renews it, unless you hard-code some method in your cron job). Perhaps I am missing something in the Certbot documentation.
(This is such an obvious and convenient feature that I'm quite surprised that Certbot, the gigantic featureful LE client that it is, doesn't support it already.)