SSH Conditional Directives

Changing ssh configuration based on laptop location
Published

April 13, 2024

I use ssh a lot, especially when I am on my laptop. Sometimes I even move my laptop. Then the directives in my ssh config file can refer to unresolvable hostnames or use incorrect proxy commands.

I have “fixed” this problem by using different ssh Host names to switch between different connection options. Today I learned about Match which allows me to automate this.

Match Documentation

The man ssh_config page has the following details about Match:

Match

Restricts the following declarations (up to the next Host or Match keyword) to be used only when the conditions following the Match keyword are satisfied. Match conditions are specified using one or more criteria or the single token all which always matches.

There is a lot more detail. The important thing to realise here is that the Match block is conditional and lasts until the next Host block (so don’t put a Host block inside it!).

Original SSH Configuration

With this we should come up with an example split configuration and go through fixing it. I have a machine I refer to as dl (deep learning). If I am on my local network I can ssh to dl.local directly as I have an entry for that in my DNS server. The problem arises when I am at work, where I have to proxy my ssh connection through a gateway box on my network.

This gives me two configurations, dl-home and dl-office for the two locations:

Host dl-home
    HostName dl.local

Host jump
    HostName i.live.here

Host dl-office
    ProxyCommand ssh jump nc %h %p

I use the jump configuration just to name the jump box.

New SSH Configuration

The difference between the two configurations is the use of the ProxyCommand. We need to apply this change only when the ssh target is the deep learning machine, and only when I am at the office. If the ProxyCommand was unconditionally applied then when I ssh to the jump box directly that would get proxied … through the jump box.

The Match directive documentation does not state this explicitly, but the conditions of the directive are all ANDed and boolean short circuiting is applied. This means that we can efficiently apply the ProxyCommand as we can first test that the ssh target is the deep learning box, and then run some command to determine if I am on my home network.

This is done with the two conditionals host and exec. The host conditional makes the Match directive only match if the current ssh target is the named host, so these two following directives are equivalent:

Host dl-home
    HostName dl.local

Match host dl-home
    HostName dl.local

The exec conditional runs a command and tests the exit status. If the command completes successfully then the condition passes. It can be negated with ! (so !exec).

To determine if I am on my home network I can test if the dl.local hostname resolves. This can be done with nslookup (dig can also do this but does not fail when the hostname is unknown):

nslookup dl.local
Server:         127.0.0.53
Address:        127.0.0.53#53

Non-authoritative answer:
Name:   dl.local
Address: ... ip v4 ...
Name:   dl.local
Address: ... ip v6 ...

When it fails:

nslookup dl.local
Server:         127.0.0.53
Address:        127.0.0.53#53

** server can't find dl.local: NXDOMAIN

With this I can craft my exec conditional:

!exec "nslookup dl.local"

The output of the command is not printed to the terminal when the test is run.

Putting it all together I get:

Host dl
    HostName dl.local

Host jump
    HostName i.live.here

Match host dl !exec "nslookup dl.local"
    ProxyCommand ssh jump nc %h %p

Now I can consistently use ssh dl no matter where I am!

It’s nice to make sure that the host conditional is first as that avoids running the nslookup command when sshing to an unrelated machine.

Testing It

It would be nice to be able to prove that it is running the exec command and correctly including or excluding the directives within. We can see this if we run ssh dl -vvv. There is a lot of output to this command, I am going to show the relevant output from a successful Match:

...
debug2: checking match for 'host dl !exec "nslookup dl.local"' host dl originally dl
debug3: /home/matthew/.ssh/config line 4: matched 'host "dl"'
debug1: Executing command: 'nslookup dl.local'
debug3: command returned status 1
debug3: /home/matthew/.ssh/config line 4: matched '!exec "nslookup dl.local"'
debug2: match found
...
debug1: Executing proxy command: exec ssh jump nc dl.local 22
...

Here you can see that:

  • the host dl conditional passes
  • the nslookup dl.local command returned exit status 1 (0 is success, everything else is failure).
  • so the !exec conditional passes
  • and later we see that the proxy command is executed

With this we can be sure that the conditional proxy command worked. (The verbose output when the match is skipped is similar, I’ve omitted it for brevity).