Fork me on GitHub

Using Bastion hosts with SSH

Sometimes companies' security policies dictate the use of "bastion hosts": Hosts that you have to "jump through" to get to the network behind the host - i.e. you cannot connect directly to a host "behind" the bastion host.

This does improve security a bit, but the most visible effect is usually to make life more difficult for people who regularly need to jump through the bastion host.

So you end up having to do things like:

you@laptop $ ssh -A bastion
...
you@bastion $ ssh real-destination
...
you@real-destination $ # Do whatever you came here to do

This is mildly annoying for interactive sessions. And it is a death knell to any automation needing to take the same route: Scripting this is not that trivial.

It is even worse if you want to copy a log file to examine locally (because the tools for analysing the file is not available on the remote network) - because then you have to copy it in two steps (and remember to clean up as you go):

you@laptop $ ssh -A bastion
you@bastion $ scp real-destination:/var/log/somelog.log ./
...
you@bastion $ exit
you@laptop $ scp bastion:./somelog.log ./
you@laptop $ ssh bastion rm ./somelog.log
you@laptop $ less somelog.log # examine the log file locally

(ok: Bad example, as "less" would generally be available, but the point is made)

The same applies if you want to upload a file to the real destination: This too has to be a two-step process.

This is a common enough scenario so SSH has support for using bastion hosts automatically. But you have to configure your SSH client accordingly to benefit from this.

If you were to add this to your $HOME/.ssh/config :

# Requires OpenSSH version >= 7.3
Host real-destination
   ProxyJump bastion

If you are running an older version of OpenSSH (e.g. if you are on a Mac or even Windows, in which case you have my sympathies), then hope is not lost:

# Requires OpenSSH version >= 5.4
Host real-destination
  ProxyCommand ssh -W %h:%p bastion

Then the nightmare of getting a log file can be reduced to the much simpler and intuitive:

you@laptop $ scp real-destination:/var/log/somelog.log ./
you@laptop $ less somelog.log # examine the log file locally

In other words, you can interact with real-destination just like any other host. As if there was no bastion getting in the way. Things like vanilla SSH also works:

you@laptop $ ssh real-destination
you@real-destination $ # do your stuff here

This does not bypass the security requirement of using the bastion host: The bastion host is being used exactly as designed. If your account is disabled on the bastion host, then you're stuck - just as you would have been before.

The only difference is that you no longer have to conciously interact with it. You can (in your mind) ignore it and concentrate your brain cycles on what you were actually trying to do instead. Possibly while snorting derisively and contemplating the fact that the nasty security guys failed to break your workflow and no longer cause any inconvenience to you :-)

Chaining Jumps

Using a bastion host like above is usually sufficient. But what if you need to jump through a series of bastion hosts to get to your destination?

SSH supports this too - you simply "chain" then together, using the same primitives as above.

Imagine we have a complicated scenario with multiple bastions, and that your user name varies between them:

  • Your laptop

  • outer-bastion - your first bastion host - with the user name of you-outer

  • inner-bastion - another bastion, only reachable from outer-bastion - with the user name of you-inner

  • destination - the host you really want to get to. Only reachable from inner-bastion. Here you use the user name of you-destination

This SSH config will helps with this:

Host destination
  UserName you-destination
  ProxyJump inner-bastion

Host inner-bastion
  UserName you-inner
  ProxyJump outer-bastion

Host outer-bastion
  UserName you-outer

There are two tricks in play here:

  • Putting the "correct" user name in the configuration with the UserName directive. This means you do not have to remember your user names in different places - when you run ssh outer-bastion (without explicitly specifying a user name) it picks up the user name from the configuration.) Less things for you to remember.

  • Chaining of the bastions: when you run ssh destination, the SSH configuration tells SSH to ProxyJump via inner-bastion. So SSH goes recursive and does that. But how does it get to inner-bastion? It has to go via outer-bastion first.

The end result is that you no longer need to clutter your brain with details about which-hoops-to-jump-through and and concentrate on your work:

you@laptop $ ssh destination
...
you@destination $ # do whatever you came to do

Enjoy!