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
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
Lets assume we have the following two hosts:
- This is our bastion host publicly reachable
- 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.
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).
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?
- How do we tell ssh to (not) use the ProxyJump?
Are we at home?
To find out if we are at home there are several options:
- Querying DNS and for some address that is different in our home network - Too complicated to set up and is a PITA with slow resolvers (ssh connections already take 3 seconds to establish on school WiFi 🙄)
- Comparing the WiFi name - This is not an option for wired networks.
- Manually - forget it I wanted to get rid of that
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.
ip a prefixes the addresses assigned to interfaces with
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.
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!
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.
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
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.
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!
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.
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.
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 🙃.
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!
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
- If your forget the
Hostrule on a
Matchyou'll get some recursion going.
Hostrule ends when a
- When you assign a variable multiple times only the value from the first assignment is taken (especially important when you want to rewrite the host for short aliases and later to localhost when redirecting into a tunnel which won't work).
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!