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.
Table of Contents
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.
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?
- 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.
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
| |
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!
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.
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.
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.
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.
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):
- If your forget the
Host
rule on aMatch
you'll get some recursion going. - A
Host
rule ends when aMatch
rule starts. - 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).
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!