2022-06-05 Tutorial: Smart ssh jumping

I'm assuming you know what an ssh jump host is, why you want it and how you configure it in your ssh ssh_config file.

The problem I'm trying to solve

SSH jumping (via JumpHost) is usually useful when you have a bastion host you have to log into to reach the network behind. In some environments you never reach this "private" network in any other way, but if the private network belongs to your home- or office and the bastion is for connections from the big scary intertubes you want to use it only when necessary, meaning we need some kind of conditional ProxyJump.

Example scenario

Lets assume we have the following two hosts:

tesla
This is our bastion host publicly reachable
hawking
This is our private host we actually want to connect to

The easy workaround

The easy solution would be to have multiple aliases and using your brain to make the decision by using the hostname when at home and the hostname-remote when elsewhere.

Example ssh configuration with different aliases for local and remote connections:
Host tesla
        HostName ssh.example.org
        User nicola
        IdentityFile ~/.ssh/id_foo
        Port 22222

Host hawking
        User stephen
        IdentityFile […]

Host hawking-remote
        ProxyJump tesla
        User stephen
        IdentityFile […]
        HostName hawking
        Port 22

Problem is that you have to type the remote alias every time you're not at home. Also using git this way is pretty annoying (you have to set up multiple remotes and manually tell git which one to use).

Smart solution

The smart solution would be that ssh somehow could figure out if it is "at home" and if not enable the proxy jump option. For that we have to answer the following two questions:

Are we at home?

To find out if we are at home there are several options:

All of the above are not really desirable, so lets think of something else … One thing that is usually frowned upon is fingerprinting, however if we turn the concept around that your local machine fingerprints its environment all of the sudden it becomes a pretty good tool.

If the year is somewhere after 2020 you probably have IPv6 configured and if you are someone like me, you probably have your own local prefix which is a pretty unique identifier for a network which the router is nice enough to tell your device when it connects!

So the magic for finding out if you are at home becomes an ip a piped into a grep -F "$ip_address".

For my purposes I wrote a little script called has-ip-grep that is a simple shell pipe returning success if a given "fingerprint" matches and fails if it does not.

The has-ip-grep script which tests if the first argument given to it matches the IP-address output part of an ip a command.
#!/bin/sh

set -e
ip a | awk '/inet/ { print $2 }' | grep -F "$1"

Explanation: ip a prefixes the addresses assigned to interfaces with inet or inet6 the awk command therefore looks for lines containing the string inet and if it finds one outputs the second column which is the IP-address. The output gets piped into a silent grep command that is told to look for instances of the first script argument if that's not the case. The set -e makes sure the script returns an error code if that match fails.

Examples

In the first example we try to match against our local IPv4 loopback interface which should always return a success, which works great for a quick test!

$ has-ip-grep 127.0.0
Example 1: Succeeds because we have the IPv4-loopback-address assigned to our lo interface.

The second example show matching against the prefix usually used by a popular series of home routers made by a German company, this is also great for testing but it will very likely not be enough to identify your home network because your neighbours and your friend's network probably have the same fingerprint.

$ has-ip-grep 192.168.178
Example 2: Usually succeeds if you are connected to a popular series of German home routers.

Other problems are that because we match substrings here this will only be convenient to match against ranges that align nicely with the decimal notation and that we get false positives, for example if someone assigns us a 10.192.168.178.

We can reduce the probability of false positives by matching against our IPv6-address, replace the fd45:6789:abcd: in the third Example with your own local IPv6-prefix. I don't recommend you rely your global unicast prefix as it is usually assigned by your ISP and may change behind your back.

$ has-ip-grep fd45:6789:abcd:
Example 3: Will succeed in your home network and maybe some other random network half around the globe you will never know exists.

So now we have a pretty good (works most of the time) way to know if we are at home lets put it to use!

Conditional ProxyJump

Luckily ssh has exactly what we need to combine it with our command we just created.

You are probably familiar with the Host keyword in your ssh configuration, turns out if you have a decently modern version of ssh there is also a Match keyword which supports some arguments.

See the manual: man 5 ssh_config

With that knowledge you can match against the target host (if you don't you'll run into funny recursion …), the network fingerprint of your choice and arrive at a configuration that can decide on the proxy without having to query your brain.

Example ssh configuration which automagically knows how to connect.
Host tesla
        HostName ssh.example.org
        User nicola
        IdentityFile ~/.ssh/id_foo
        Port 22222

Host hawking
        User stephen
        IdentityFile […]

Match Host hawking !Exec "has-ip-grep '192.168.42' && has-ip-grep 'fd42:f00b:aba2:'"
        ProxyJump tesla
        HostName hawking
        Port 22

Explanation

The configuration for tesla stays the same (assuming we only want to connect to it from the public side). The configuration for the local connection to hawking can stay too, the Host hawking-remote directive was replaced with a Match that tests if we want to connect to hawking and then executes our shell command.

In this case we check for two IP-addresses that we expect to get assigned in our private network, As I'm running dual stack it won't hurt to check against the IPv4-address too.

These two commands are combined with some shell syntax and quoted using double-quotes because that's how ssh_config works. The Exec is prefixed with an exclamation mark because we want to match if we are NOT at home. The part after that is the same as with the remote alias except for the User and IdentityFile because we now the Host hawking always matches and we don't have to duplicate them anymore.

I can imagine it being a lot of fun configuring several Networks this way 🙃.

Security

This is by no means any kind of security measure, if the network you are connected to matches the fingerprint of your private network this will make the wrong decision to connect directly! However if you are running such a setup you should have the certificate fingerprints of your real servers already in your known_hosts file and then just be smart enough to not accept any new certificates over untrusted connections.

If that finger print matches when you are not in your network (assuming you randomly chose an IPv6-prefix) you should get the hell out of there!

Debugging

ssh -v is your friend!

If ssh seems to do nothing except burning CPU cycles you might have ran into some recursion problem, Make sure that your Match keywords always match on some host and that your proxy connections aren't jumping in circles!

Also some pits I fell into (all of them are documented in the ssh_config man-page):

That's it!

Have fun connecting to your servers without having to think about changing names! If you spotted a mistake somewhere or have other feedback feel free to contact me!