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
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
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
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
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
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
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
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
Match keywords always match on some host
and that your proxy connections aren't jumping in
Also some pits I fell into (all of them are documented in
- 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!