I have an Intel NUC at home that I use as an HTPC and a sort-of single-machine homelab (which seems to be the in vogue term computer nerds are using for “the machine(s) I run at home for fun”). As an HTPC, it’s connected to my TV and is how I do a lot of film and TV watching. As a homelab, it’s useful for projects I’d prefer not to run on my laptop - because, say, it may involve long-running tasks, and if I have to ensure my laptop is constantly on & connected, it defeats the purpose of having a laptop.
My development environment is pretty much Vim and Tmux regardless of where
I’m working, so I can ssh to the server from my local network and work the same
way I do on my laptop. To make this a bit easier, I added a block to the
~/.ssh/config on my laptop like:
Host htpc User myusername ForwardAgent yes Hostname htpc.local
This allows me to just ssh htpc rather than needing to type ssh email@example.com.
So having a computer that isn’t a laptop and just sits, at home, always running is useful, but the flip side, of course, is that if I’m away from home and want to check in on one of the projects I’m running on that machine, that’s a hassle, since the machine isn’t with me.
Step 1 was doing the obvious thing and opened up SSH connections from outside my
local network. I’m not going to go through all the details of doing this safely
here, as there’s lots of resources already online, but it is important to
quickly mention that this is dangerous if you aren’t careful with your
configuration. If you aren’t careful, you can easily make it possible for a bad
actor to SSH into your home network from anywhere. Even if you don’t intend to
open up your machine to SSH from the Internet, a lot of the same
security-concious settings are good to apply. The key things I would recommend
sshd pay attention to:
- Restrict which users can log in via
sshat all, either via an explicit list of users (
AllowUsers) or a group (
- Regardless of how you restrict which users can login, ensure
rootisn’t one of them. That means
PermitRootLogin noin your
- Only allow authentication via keys - never passwords. Configuration options
for this include
ChallengeReponseAuthentication no(the second one is easy to forget!).
- Use a non-standard
Port <number>). This one falls under “security by obscurity”, so don’t count on it as a real layer of defense: if you don’t secure everything else correctly, this probably won’t save you. But it’s still worth doing – if nothing else, it will dramatically reduce the number of bots that try to break in via port-scanning, which isn’t harmful if everything else is configured correctly, but can be annoying.
After doing all that, I need to make sure I can actually reach my home server
from the Internet, so I added a port routing rule to my Wi-Fi router so that
incoming connections on the custom port I chose for
sshd are routed to my home
server’s IP. (I also have a static IP allocation for the home server so its
local IP address will never change.) Lastly, I set up a DNS record for a domain
name I own to point at my external IP. You could also use a service like
Dyn to register a domain and keep it up to date
automatically: I didn’t bother since my external IP almost never changes so I
don’t feel the need to automate this, and since being acquired by Oracle Dyn
doesn’t seem like as good a service as it used to be.
With all that done, I added a new block to
~/.ssh/config so that when I’m away
from home I can access my home server with ssh htpc-remote:
Host htpc User myusername ForwardAgent yes Port 12345 Hostname htpc.local Host htpc-remote User myusername ForwardAgent yes Port 12345 Hostname myhtpc.example.com
That’s pretty convenient, but it does mean my muscle memory gets tripped up: I’m
usually at home, so I’m used to just running ssh htpc, but now I have to
remember to type ssh htpc-remote sometimes. I could just change the
htpc config to
Hostname myhptc.example.com, but I see no reason to deal with
additional latency when the target machine is a few feet away. It’d be nicer if
I could run ssh htpc and have it dynamically choose the best hostname.
Everything up until now was prelude to the “one weird trick” that makes this
Match directive. The
Match directive allows elementary “if”
logic in your
ssh_config - you can match on things like the hostname being
connected to, the local user running
ssh, or the result of an arbitrary
exec to alter the options applied to the connection. We’re going
exec to make this work.
The first thing we need is a shell script which can detect if we’re on our home Wi-Fi. Here’s mine:
#!/bin/sh # # Exit 0 if I'm on my home network, 1 otherwise set -e network="MyHomeNetwork" iwctl station wlan0 show | grep --quiet "Connected network.*$network"
I’ve saved this to
chmod +x-ed it. The details of
how you’d implement this depend on your operating system and how your Wi-Fi
connection is managed. On recent versions of MacOS, networksetup
-getairportnetwork en0 should include your current network name. On Linux, I
leave this as an exercise to the reader:
iwd is what I use to manage Wi-Fi,
you’d need to adjust the commands if you use
netctl, or something else. (If you
use Linux and aren’t sure what network manager you use, it’s probably
NetworkManager, I think it’s currently the most popular default choice for
distributions like Ubuntu).
Now that we’ve got this script ready, we can make our final change to
Match host htpc !exec is-home-wifi Hostname myhtpc.example.com Host htpc User myusername ForwardAgent yes Port 12345 Hostname htpc.local
Match line is operating this way:
host htpcmatches the host when we run
!exec is-home-wifiruns the
is-home-wifiscript we wrote: if it exits with status
0(success), then the condition matches.
!negates the condition.
So in pseudo-code, this can line be read as
if host == "htpc" and not home wifi.
When the condition matches, the next
Hostname line indicates which hostname to
actually connect to, overriding the default value configured under the
Intuitively you might think the
Match block should go after the
block, but this doesn’t work. I don’t fully understand how the SSH config is
parsed and at what stage the
Match is evaluated and applied, but if the
block is put at the bottom, the
host htpc condition no longer matches -
host htpc.local does match, but then the
Hostname change is no longer
ssh -vvv output says the match was applied. If anyone can
explain the vagaries of SSH config evaluation to me, I’d be interested to learn
With all of that, I can now run ssh htpc from anywhere, and be routed to the most appropriate hostname for my current network.